diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..9e8e4f7f5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + groups: + weekly-batch: + patterns: + - "*" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b3e2be23b..caab4c3b7 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -36,39 +36,23 @@ env: VENV_PATH: .venv jobs: - # Wait for the testing pipeline to finish - wait-for-testing: - name: πŸ•’ Wait for Testing Pipeline - runs-on: ubuntu-latest + # Run the test workflow as a prerequisite for release builds. + test: + name: πŸ§ͺ Run tests + uses: ./.github/workflows/testing.yaml if: ${{ github.repository == 'crs4/rocrate-validator' }} - steps: - - name: Wait for testing pipeline to succeed - uses: fountainhead/action-wait-for-check@v1.2.0 - id: wait-for-testing - with: - token: ${{ secrets.GITHUB_TOKEN }} - checkName: βŒ› Run tests - ref: ${{ github.sha }} - - - name: Do something with a passing build - if: steps.wait-for-testing.outputs.conclusion == 'success' - run: echo "Testing pipeline passed" && exit 0 - - - name: Do something with a failing build - if: steps.wait-for-testing.outputs.conclusion == 'failure' - run: echo "Testing pipeline failed" && exit 1 # Check and Build Distribution build: name: πŸ— Check and Build Distribution runs-on: ubuntu-latest - needs: wait-for-testing + needs: test if: ${{ github.repository == 'crs4/rocrate-validator' }} steps: - # Access the tag from the first workflow's outputs + # Checkout the commit associated with the release tag. - name: ⬇️ Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: 🐍 Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.x" - name: 🚧 Set up Python Environment @@ -156,7 +140,7 @@ jobs: name: python-package-distributions path: dist/ - name: πŸ–ŠοΈ Sign the dists with Sigstore - uses: sigstore/gh-action-sigstore-python@v3.3.0 + uses: sigstore/gh-action-sigstore-python@v3.5.0 with: inputs: >- ./dist/*.tar.gz diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index a86581955..f7e463f9a 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -14,14 +14,11 @@ name: πŸ§ͺ CI Pipeline 1 -- Testing -# Controls when the action will run. Triggers the workflow on push or pull request -# events but only for the master branch +# Runs on pushes and pull requests, and can be called by the release workflow. on: push: branches: - "**" - tags: - - "*.*.*" paths: - "**" - "!docs/**" @@ -31,6 +28,7 @@ on: - "**" - "!docs/**" - "!examples/**" + workflow_call: env: TERM: xterm @@ -46,9 +44,9 @@ jobs: steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - name: ⬇️ Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: 🐍 Set up Python v${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: ${{ env.PYTHON_VERSION }} - name: πŸ”½ Install ruff (version from pyproject.toml) @@ -74,7 +72,7 @@ jobs: # Emit a compact rule-count summary and fail the job if needed. ruff check rocrate_validator tests --statistics - name: βŒ› Spell check code and profiles (covers Python and SHACL) - uses: crate-ci/typos@v1.47.0 + uses: crate-ci/typos@v1.51.1 # Runs the tests test: @@ -83,9 +81,9 @@ jobs: needs: [lint] steps: - name: ⬇️ Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: 🐍 Set up Python v${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: ${{ env.PYTHON_VERSION }} - name: πŸ”„ Upgrade pip diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 370ed027a..e2fad595d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: # Run typos to check for common spelling mistakes in the codebase. - repo: https://github.com/crate-ci/typos - rev: v1.47.2 + rev: v1.50.3 hooks: - id: typos # Pre-commit passes explicit file paths to the hook, bypassing the diff --git a/CHANGELOG.md b/CHANGELOG.md index f818785aa..7fec66c49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,287 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +## [0.13.1] - 2026-10-29 + +Full changelog: https://github.com/kikkomep/rocrate-validator/compare/0.13.0...0.13.1 + +### πŸ”§ Changed + +- chore(deps): update dependencies in poetry.lock, including PySHACL 0.40.2 and Starlette 1.10.0 ([c27cf006](https://github.com/kikkomep/rocrate-validator/commit/c27cf006536fcdd0b2e5ea3c8916c88d5ba5766f)) +- build(deps): allow Rich versions below 15 for compatibility with Flower 1.32+ ([64ded524](https://github.com/kikkomep/rocrate-validator/commit/64ded52427045db229a8adf6c76c7c076949920f)) +- build(deps): bump crate-ci/typos ([e022f654](https://github.com/kikkomep/rocrate-validator/commit/e022f6549358fd5d30ab7a9e3a68354d5aab7c5b)) + +### πŸ› Fixed + +- fix(profiles): use resolved IDs for web entity checks ([49638aa7](https://github.com/kikkomep/rocrate-validator/commit/49638aa7d8ab2ee87c280a74fea2bdc15ca0ae0e)) +- fix(crate): resolve entity IDs against JSON-LD `@base` ([e9ef43ed](https://github.com/kikkomep/rocrate-validator/commit/e9ef43ed0f4d1a3a1a8c3e65b37809deca865e6e)) +- fix(uri): handle local paths with URI-safe conversion ([4bdea490](https://github.com/kikkomep/rocrate-validator/commit/4bdea4903b85eacdb3bbe8ddcd3a22848033036e)) + +## [0.13.0] - 2026-10-21 + +Full changelog: https://github.com/kikkomep/rocrate-validator/compare/0.12.2...0.13.0 + +### ✨ Added + +- Profile detection now explains suggested profiles and lets users confirm them before validation + ([2b0cbaae](https://github.com/kikkomep/rocrate-validator/commit/2b0cbaae32ab53989536e1a6662d9d3f6bbb476f)). +- Opt-in extra checks extend validation beyond the standard profile rules + ([e3ef940b](https://github.com/kikkomep/rocrate-validator/commit/e3ef940b1a07ce95c9fa86358114d619f0b67aa5), + [8b0d1fcd](https://github.com/kikkomep/rocrate-validator/commit/8b0d1fcd39c77e86602ffe40aceba34545e9bfc5)). +- Automatic package-type detection and CLI selection distinguish attached and detached crates; + reports show the package type and validation scope + ([bebee42b](https://github.com/kikkomep/rocrate-validator/commit/bebee42bad8783fd696bcaa51a1cd5fbcf25ff35), + [06c29319](https://github.com/kikkomep/rocrate-validator/commit/06c29319284ab982ab6fc6fe3ded716ec366aeb4), + [595fcc8e](https://github.com/kikkomep/rocrate-validator/commit/595fcc8ef5f3504acd52014eac33de31717c82b5)). +- Dictionary metadata can be validated against files in a local package root + ([86a84ea5](https://github.com/kikkomep/rocrate-validator/commit/86a84ea536b50ef2f3e60b1aa4a07540b7152420)). +- Reusable validation preparation avoids repeating setup across validation calls + ([6bd45817](https://github.com/kikkomep/rocrate-validator/commit/6bd45817e2be936179ff007d727a0450eef8d6ba)). +- Workflow Run Crate checks require an action using the main entity as its instrument and support + additional action types + ([d88a1ce8](https://github.com/kikkomep/rocrate-validator/commit/d88a1ce841956a9a3df5f2049397b257d13b6672), + [57a1413f](https://github.com/kikkomep/rocrate-validator/commit/57a1413f3c1293bd7de673f7eb072ac31d07bc7c)). + +### πŸ› Fixed + +- Reusing prepared profiles across crates preserves absolute IRIs, relative identifiers, URI paths, + and SHACL class targets + ([dc258c3a](https://github.com/kikkomep/rocrate-validator/commit/dc258c3a9a384df9cee9181151d67d9e4a936c5f), + [5edb72e4](https://github.com/kikkomep/rocrate-validator/commit/5edb72e4c19e41eb2b839293bb969610909722cc), + [230856a1](https://github.com/kikkomep/rocrate-validator/commit/230856a199688e7af3dc6cd49bc462de36b2f910)). +- Package checks handle attached metadata-only inputs and dictionary metadata consistently; + reports identify the local package root + ([858a0d3c](https://github.com/kikkomep/rocrate-validator/commit/858a0d3c462b0cd6c0c15c7993772579651577b2), + [0c9967c0](https://github.com/kikkomep/rocrate-validator/commit/0c9967c02a42af5a8df443634c064449fe7c75e7), + [545e7448](https://github.com/kikkomep/rocrate-validator/commit/545e744825b4106b58aa549c6c8331c74630ae46)). +- Malformed profile hints and invalid JSON produce clearer messages, while unexpected detection + errors are no longer hidden + ([756dbb4e](https://github.com/kikkomep/rocrate-validator/commit/756dbb4e4469e855544e964010e30f2fbc102a4b), + [7a95271f](https://github.com/kikkomep/rocrate-validator/commit/7a95271f9f1dc0d2899dafb370b8dc9dbda2826c)). +- Removed duplicate extra-check tables from CLI output + ([9d7d4b76](https://github.com/kikkomep/rocrate-validator/commit/9d7d4b763f8e0db550922132f011837464df525c)). +- Fixed Sphinx crashes when including README sections and improved dark-mode readability + ([153efa65](https://github.com/kikkomep/rocrate-validator/commit/153efa655ca9b210fe3f796e0ba3bf3a58543e8d), + [f9b8822d](https://github.com/kikkomep/rocrate-validator/commit/f9b8822d59ebe54ebf3f44169adfd11d91f64e63)). + +### πŸ“š Documentation + +- Redesigned documentation with the Shibuya theme, clearer navigation, and live preview with + automatic rebuilds + ([c0212aaa](https://github.com/kikkomep/rocrate-validator/commit/c0212aaa79c91b720d2376752f2d466dfadb9504), + [83a84c4a](https://github.com/kikkomep/rocrate-validator/commit/83a84c4a02b0a66d8e476c6d6dfd075edfa2028c), + [d955005f](https://github.com/kikkomep/rocrate-validator/commit/d955005f381ce62cd7db973cb8ea504b85c8083c)). +- Expanded guides for profile detection, extra checks, package modes, and reusable validation + ([05f0dc30](https://github.com/kikkomep/rocrate-validator/commit/05f0dc305c89cde0e6f13553e1cdaeac81b09a33), + [2f4f97c5](https://github.com/kikkomep/rocrate-validator/commit/2f4f97c5c153dcb96829021808f732fe82965ce8), + [3b1a66e9](https://github.com/kikkomep/rocrate-validator/commit/3b1a66e95779d460b30388e905c76330128800ea), + [4c88ced7](https://github.com/kikkomep/rocrate-validator/commit/4c88ced7124ec4cea2caae45d1eacb85489c609d)). + +### ⚑ Performance + +- Reuse prepared profiles across crate bases and selected profiles when computing statistics, + reducing repeated profile loading and preparation + ([08d317e1](https://github.com/kikkomep/rocrate-validator/commit/08d317e1aae9377daed99c43505ca069da580798), + [401b8d64](https://github.com/kikkomep/rocrate-validator/commit/401b8d64b39182983f8ed446b8789fa4dc9e2f88)). + +## [0.12.2] - 2026-10-05 + +Full changelog: https://github.com/kikkomep/rocrate-validator/compare/0.12.1...0.12.2 + +### πŸ› Fixed + +- fix(profiles): handle absolute file URI Data Entity identifiers ([afb7b0d2](https://github.com/kikkomep/rocrate-validator/commit/afb7b0d2b5e245f81f5f36f6d1ade43b0f2c53cb)) +- fix(profiles): accept declared compact IRIs in JSON-LD properties ([050dd18a](https://github.com/kikkomep/rocrate-validator/commit/050dd18a2e3185d490ea95ee2d68cbb0470903a8)) + +### πŸ”§ Changed + +- test(profiles): cover absolute file URI Data Entities ([9f12712b](https://github.com/kikkomep/rocrate-validator/commit/9f12712b3543afd61f9b8b559f32d37527669ae4)) +- test(jsonld): cover compact IRI prefixes across RO-Crate profiles ([cdb6e04e](https://github.com/kikkomep/rocrate-validator/commit/cdb6e04edcfba8a8eb29b36a443ce3e631f008d4)) + +## [0.12.1] - 2026-10-01 + +Full changelog: https://github.com/kikkomep/rocrate-validator/compare/0.12.0...0.12.1 + +### πŸ”§ Changed + +- chore(deps): group weekly dependency updates ([088d7f43](https://github.com/kikkomep/rocrate-validator/commit/088d7f4310e245d989bc25058d088558fa7d3d16)) +- test(validation): cover metadata descriptor targeting across core versions ([a09b795a](https://github.com/kikkomep/rocrate-validator/commit/a09b795a0f9945397fd0d1583edd27d1e6629ae9)) +- ci(deps): configure weekly Dependabot updates for GitHub Actions ([039f8f13](https://github.com/kikkomep/rocrate-validator/commit/039f8f136d7062122314c44fbb1644cab00e73fb)) +- ci(release): update Sigstore action to v3.5.0 ([42896acd](https://github.com/kikkomep/rocrate-validator/commit/42896acda7075c5b5ed776a95a91a1210653e68f)) +- ci(typos): align CI and pre-commit on v1.50.3 ([d531e457](https://github.com/kikkomep/rocrate-validator/commit/d531e4576e7a0a33742ed2f30ddbbbfb6c480f56)) +- ci(actions): update setup-python to v7 ([0b33ccf6](https://github.com/kikkomep/rocrate-validator/commit/0b33ccf643d4a1df9e6b17068a334d437581b08e)) +- ci(actions): update checkout to v7 ([e0ccc19d](https://github.com/kikkomep/rocrate-validator/commit/e0ccc19df2c153fa186546c49fe5e649c41b26c9)) +- ci: replace release check polling with native workflow dependency ([54560d6a](https://github.com/kikkomep/rocrate-validator/commit/54560d6ad6460497ea611f9d0f2b4c6ec290be1e)) +- test(validation): verify effective overlay IDs in text reports ([c51cf5de](https://github.com/kikkomep/rocrate-validator/commit/c51cf5de4362f4d4da92b4e40a070aa7a17e683b)) +- test(validation): cover profile Root Data Entity hasPart cases ([865f39c7](https://github.com/kikkomep/rocrate-validator/commit/865f39c729f44d09456ab605fa44702680395d26)) + +### πŸ› Fixed + +- fix(profiles): identify metadata descriptors by filename suffix ([06f5520e](https://github.com/kikkomep/rocrate-validator/commit/06f5520ec4866afaf2eb6bae59a1ddb52781b132)) +- fix: correct two check descriptions ([e150f0dc](https://github.com/kikkomep/rocrate-validator/commit/e150f0dcadf6e43b0e6362ed547d2c0bda1aaad9)) +- fix(validation): show overlay check IDs in text reports ([def3ded1](https://github.com/kikkomep/rocrate-validator/commit/def3ded1189e4861afea4af8f868fd64e7f714c3)) +- fix(validation): report missing hasPart Data Entities accurately ([6c065b84](https://github.com/kikkomep/rocrate-validator/commit/6c065b84709f9ec663675ac1c3f168c47cf798f7)) + +## [0.12.0] - 2026-09-30 + +Full changelog: https://github.com/kikkomep/rocrate-validator/compare/0.11.4...0.12.0 + +### ⚠️ Breaking Changes + +- **The default validation profile is now `ro-crate-1.3`** (previously `ro-crate-1.1`). Since 1.3 + is self-contained, validation now applies 95 requirements instead of 30; crates declaring the + 1.1 context fail the `ro-crate-1.3_2.2` check under the default profile. This primarily affects API + users when `profile_identifier` is omitted. CLI auto-detection is unchanged unless no profile is + detected or `--no-auto-profile` is used. To keep the previous behaviour, explicitly select + `ro-crate-1.1` with `profile_identifier` or `-p ro-crate-1.1` + ([b6199c60](https://github.com/kikkomep/rocrate-validator/commit/b6199c608df4b1d20e987a4946d06005b04fe795)). + +### ✨ Added + +- feat(profiles): support rule overlays through `validator:isRuleOverlayOf`, allowing a target profile + to reuse checks from a direct parent while retaining source provenance and reporting target-relative + effective identities. Overrides defined by the target profile match parent checks by + `(name, severity)` ([e92e826b](https://github.com/kikkomep/rocrate-validator/commit/e92e826bd2b2e49b36775f5150c81f50089439fb)). +- feat(profile-checks): add an extensible profile consistency framework that detects duplicate check + identities, invalid overlay relationships, and ambiguous checks inherited from multiple sources + ([59a66303](https://github.com/kikkomep/rocrate-validator/commit/59a6630320b4303b0ef6c22020371dafb1b01891), + [99748956](https://github.com/kikkomep/rocrate-validator/commit/997489564c1065df9fc71f36844271bfffe5d4a2)). +- feat(cli): add `profiles check ` and automatic profile validation, with + `--no-profile-checks` and the corresponding API setting available as explicit opt-outs + ([a4eb3fd2](https://github.com/kikkomep/rocrate-validator/commit/a4eb3fd2868b91a511bb993b73910ad0b36225de), + [f430b2b9](https://github.com/kikkomep/rocrate-validator/commit/f430b2b95604be431956e4cdc5f08229a853722a)). +- feat(api): expose effective provenance for checks defined by the target profile, inherited checks, + and replacement checks in validation events, issues, statistics, and skipped-check results + ([a7fabdb4](https://github.com/kikkomep/rocrate-validator/commit/a7fabdb47b2d974894039fb80b4730b65a9d4292)). +- feat(cli): show source and effective check identities and their provenance relationships in verbose + `profiles describe` output ([b4108c16](https://github.com/kikkomep/rocrate-validator/commit/b4108c160a0eb51a34e0d91e7b16d91520e477fc)). +- feat(profiles): provide comprehensive RO-Crate 1.3 validation coverage for metadata descriptors, + entities, workflows, and detached or referenced crates + ([5a98e4e0](https://github.com/kikkomep/rocrate-validator/commit/5a98e4e02ce60bd5e61e3be943ceea8bba166e25), + [e2bc9846](https://github.com/kikkomep/rocrate-validator/commit/e2bc984662408160535c25a803300c905c58216e), + [41cd9fc3](https://github.com/kikkomep/rocrate-validator/commit/41cd9fc3be279ad8c15effd6d891f4770cf2dafb), + [bc2f76e4](https://github.com/kikkomep/rocrate-validator/commit/bc2f76e46d1e047a5c36a09e417d59146143b17a)). +- feat(validation): support tri-state check outcomes and declared dependencies between checks, and + propagate dependency outcomes through validation + ([80495af4](https://github.com/kikkomep/rocrate-validator/commit/80495af484f6a60c2f7fa6fae389c52b8c9dfa3a), + [77c4741b](https://github.com/kikkomep/rocrate-validator/commit/77c4741bf8e1cdb28627906ed7bd8c2611708e7f), + [7a5a1749](https://github.com/kikkomep/rocrate-validator/commit/7a5a17497426e5cd86eb219ca5ca2c5df2fe53ec)). +- feat(cli): report structured skipped-check reasons in validation results and aggregate them in JSON + output ([c3044802](https://github.com/kikkomep/rocrate-validator/commit/c30448029d979b6af44d0ccf67831e8076e2eaf4), + [035e5485](https://github.com/kikkomep/rocrate-validator/commit/035e5485c7f439c927bd1709ad6f5b83b04b87ca), + [892460ec](https://github.com/kikkomep/rocrate-validator/commit/892460ece97b738a6c9d1c4ee093983cc5d69df2), + [70c47ab5](https://github.com/kikkomep/rocrate-validator/commit/70c47ab5a0b4c93ed326e57ad0d06a24121d4c8c)). +- feat(graph): add an extensible graph-transformer pipeline for preparing validation graphs and + selecting candidate entities from SHACL shapes + ([80531e0a](https://github.com/kikkomep/rocrate-validator/commit/80531e0add141bd6a439eef73a298138faceeebd), + [74aca87e](https://github.com/kikkomep/rocrate-validator/commit/74aca87ed604be2dd1f4c0c3df225f645c35ed9b), + [3a8d0cf0](https://github.com/kikkomep/rocrate-validator/commit/3a8d0cf054e21ed2cef2863dca62496988aa12b5), + [822c26be](https://github.com/kikkomep/rocrate-validator/commit/822c26be8073baf015ed5e09e8759b9aaea128e6)). +- feat(profiles): validate Schema.org IRI protocols with SHACL + ([7f6e8032](https://github.com/kikkomep/rocrate-validator/commit/7f6e8032a8cc2354dfb3a49f35eba81c091c0828)). +- feat(validation): propagate check execution errors and render them safely in the CLI + ([f2e7f36a](https://github.com/kikkomep/rocrate-validator/commit/f2e7f36a9a4fa2b7d77341e3e075bfbd8199aada), + [48bf2968](https://github.com/kikkomep/rocrate-validator/commit/48bf296844d3868e52c161d300c7d7c11a4ea01f)). + +### πŸ”§ Changed + +- refactor(validation): resolve bare profile tokens to the highest available version using numeric + ordering and store the resolved identifier so downstream consumers use the applied profile + ([7b782975](https://github.com/kikkomep/rocrate-validator/commit/7b7829750bac3a6b28293310d6f5de4b71c2ae0b)). +- refactor(cli): use consistent styling for profile names, identifiers, check names, and provenance + labels across profile commands ([40e7fe4e](https://github.com/kikkomep/rocrate-validator/commit/40e7fe4e07cdef914f99e41434158cf271c98ad3), + [2316a430](https://github.com/kikkomep/rocrate-validator/commit/2316a430f9c4393597af794754c20bdb0c27b086)). +- refactor(shacl): separate graph-transformer implementations from the SHACL validation core + ([21590fe8](https://github.com/kikkomep/rocrate-validator/commit/21590fe88be1511195c8d3b9051a4244a5f33bed), + [a46eb34f](https://github.com/kikkomep/rocrate-validator/commit/a46eb34ff08c562fbc5d3a4109cf6809eea1338e)). +- refactor(profile): declare RO-Crate 1.3 as a rule overlay over RO-Crate 1.2 and rename the overlay + property to `isRuleOverlayOf` + ([9dd09229](https://github.com/kikkomep/rocrate-validator/commit/9dd09229f1e4e1e385dbc95e5cf7d3322f393cc2), + [82df8dca](https://github.com/kikkomep/rocrate-validator/commit/82df8dcae7b280774f4d83bdba71336a0d19b5dc)). + +### πŸ› Fixed + +- fix(validation): preserve general-to-specific overlay traversal and source check execution order; + resolve the overlay target from `profile_identifier` and preserve inherited dependencies and skip + settings independently of profile loading order + ([a73f90da](https://github.com/kikkomep/rocrate-validator/commit/a73f90da12b10eca5681497d20cc9432f145245f), + [dbea5738](https://github.com/kikkomep/rocrate-validator/commit/dbea5738c6adfd56eca8ee1ed2595d0a78c75bba), + [996b52aa](https://github.com/kikkomep/rocrate-validator/commit/996b52aa561acb6c00f0c24c911f57bd409919b3), + [709e7564](https://github.com/kikkomep/rocrate-validator/commit/709e75641dfc2ed8115154d7ff15f0578d5d252a)). +- fix(shacl): load each overlay source graph once and evaluate the merged shapes graph at the target + profile ([22455d3b](https://github.com/kikkomep/rocrate-validator/commit/22455d3be43fc6425ddb76161d8d67d7ed53b660)). +- fix(profiles): reject ambiguous effective identities instead of implicitly selecting one inherited + check, and disambiguate legacy duplicate check names within existing profiles + ([f00ea8c1](https://github.com/kikkomep/rocrate-validator/commit/f00ea8c11f73cd62f180814e52b11f72fac1ea7a), + [16709a69](https://github.com/kikkomep/rocrate-validator/commit/16709a69d6ac06ad22dc8e7a7e392b8afd875821)). +- fix(graph): treat `schema:additionalType` as a reference predicate + ([ce4856c9](https://github.com/kikkomep/rocrate-validator/commit/ce4856c93bf869370bc07530a120c87a2a91c6a5)). +- fix(validation): correct skipped-check counts, align check statistics with validation outcomes, and + keep batched SHACL results synchronized with live progress + ([0416446b](https://github.com/kikkomep/rocrate-validator/commit/0416446bade7776120f112ee58d64fdffd1b79e5), + [39f3f640](https://github.com/kikkomep/rocrate-validator/commit/39f3f6409973975ab7710cc1b83c6d8c6727b0c2), + [d4ab2cb8](https://github.com/kikkomep/rocrate-validator/commit/d4ab2cb84e45cbe5a57b642aa878ba0e943724d1)). + +### πŸ“š Documentation + +- docs(profiles): document ordinary inheritance, check overrides, rule overlays, deactivation, and + source versus effective identities with concrete SHACL and Python examples + ([3dd0b793](https://github.com/kikkomep/rocrate-validator/commit/3dd0b793296a699312d621c14c23f75e7f5a768f)). +- docs(api): document effective check provenance for composed profiles + ([12485e89](https://github.com/kikkomep/rocrate-validator/commit/12485e8984873e35672c0be805ba7a8d8252215d)). +- docs(graph): document the graph-transformer extension points and SHACL implementation catalog + ([782f74c4](https://github.com/kikkomep/rocrate-validator/commit/782f74c4d9e90919412ffc41fa50d6b86b5505bc), + [fc455598](https://github.com/kikkomep/rocrate-validator/commit/fc455598486d4288ce39f81abb82ea6da0813ed7)). + +### ⚑ Performance + +- perf(validation): cache profile consistency results and effective check provenance within a + validation session to avoid repeated resolution work + ([c0b65cad](https://github.com/kikkomep/rocrate-validator/commit/c0b65cad72434db97e3cf56405b57a65e1462e80), + [8df06f12](https://github.com/kikkomep/rocrate-validator/commit/8df06f12df83a2c3e62fce1b96fc879c5587ecfe)). + + +## [0.11.4] - 2026-09-16 + +Full changelog: https://github.com/crs4/rocrate-validator/compare/0.11.3...0.11.4 + +### ✨ Added + +- feat(rocrate): add validation for singleton arrays in compacted entity properties ([9d8078c7](https://github.com/crs4/rocrate-validator/commit/9d8078c75e7482c78928d9929784e6d3c6e6f71f), [675cdb27](https://github.com/crs4/rocrate-validator/commit/675cdb2788c57bc4b4216642cb146346730d8a5a), [2aa6bcba](https://github.com/crs4/rocrate-validator/commit/2aa6bcba5886e36ab92e0f5c505af479eea7e414)) + +### πŸ”§ Changed + +- refactor(metadata): expose raw metadata descriptor bytes for encoding checks ([e18e24ef](https://github.com/crs4/rocrate-validator/commit/e18e24ef224493dbfe6acb73a3e494b3351bcf85)) +- refactor(versioning): use a shared distribution name when resolving the package version ([e57e2ea7](https://github.com/crs4/rocrate-validator/commit/e57e2ea701651f748aa6bf139158ac0bb051493f)) + +### πŸ› Fixed + +- fix(validation): skip metadata-dependent profile checks when descriptors are missing or unreadable instead of reporting misleading errors ([b302b5cd](https://github.com/crs4/rocrate-validator/commit/b302b5cdd1dd25678e5bbc3d1a723f2ce87e7c8a), [2444e97a](https://github.com/crs4/rocrate-validator/commit/2444e97ad3d38ae41c5d3fcd8b11cfa5ee13c9ec)) +- fix(metadata): handle missing metadata descriptors without leaking `FileNotFoundError` ([e1217fda](https://github.com/crs4/rocrate-validator/commit/e1217fda31b7e8c806202350f8c17150735813d5)) +- fix(profile): skip descriptor encoding validation in metadata-only mode and narrow exception handling ([acb715bf](https://github.com/crs4/rocrate-validator/commit/acb715bfc9c5ae3f27f399aae8e429660a311ebe), [9411c86c](https://github.com/crs4/rocrate-validator/commit/9411c86c195b6aad8bdf1b4bd13738ec9995f580)) +- fix(rocrate): normalize singleton-array `mainEntity` values in workflow crates ([568b8c41](https://github.com/crs4/rocrate-validator/commit/568b8c414541de31410c513901573cc617fa2aec)) +- fix(profiles): use the canonical namespace for the RO-Crate 1.2 ontology ([d9e115cb](https://github.com/crs4/rocrate-validator/commit/d9e115cb71ee11bff7422b68ea6b6351b432360c)) +- fix(versioning): resolve the package version when installed outside a Git checkout ([856546a4](https://github.com/crs4/rocrate-validator/commit/856546a49d61771c478b0a232bc12501313a46e0)) + +### πŸ“š Documentation + +- docs(profiles): document SHACL target selection optimization ([14058e67](https://github.com/crs4/rocrate-validator/commit/14058e67fd96043ed5298c91c6396293e0a1d484)) + +### ⚑ Performance + +- perf(profiles): replace custom SPARQL targets with SHACL Core targets ([bb4d0317](https://github.com/crs4/rocrate-validator/commit/bb4d0317ef28c91470b9da46f5048d5d940f49aa)) +- perf(profiles): reduce duplicate and cross-product work in contextual-entity target queries ([be6aa6ff](https://github.com/crs4/rocrate-validator/commit/be6aa6ffe98cb55869c6e9b57634d08e1cc53f0d), [88ded678](https://github.com/crs4/rocrate-validator/commit/88ded678cd0009df06209d8a48bbc7346ae5fbc6)) + +## [0.11.3] - 2026-07-28 + +Full changelog: https://github.com/crs4/rocrate-validator/compare/0.11.2...0.11.3 + +### πŸ› Fixed + +- fix(cli): disable Rich word-wrap in the JSON validation report to prevent literal newlines inside quoted strings that produced invalid JSON ([ab66c773](https://github.com/crs4/rocrate-validator/commit/ab66c773)) + ## [0.11.2] - 2026-06-25 Full changelog: https://github.com/crs4/rocrate-validator/compare/0.11.1...0.11.2 diff --git a/README.md b/README.md index f554c7424..334a48bca 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,9 @@ against different profiles, including the base RO-Crate profile and various exte - Validates RO-Crates against the profiles they declare to conform to. Currently, validation for the following profiles is implemented: - - [RO-Crate](https://w3id.org/ro/crate/1.1) *(base profile)* + - [RO-Crate 1.3](https://w3id.org/ro/crate/1.3) *(base profile, used by default)* + - [RO-Crate 1.2](https://w3id.org/ro/crate/1.2) *(base profile)* + - [RO-Crate 1.1](https://w3id.org/ro/crate/1.1) *(base profile)* - [Workflow RO-Crate](https://w3id.org/workflowhub/workflow-ro-crate/1.0) - [Workflow Testing RO-Crate](https://w3id.org/ro/wftest) - [Workflow Run Crate](https://w3id.org/ro/wfrun/workflow) @@ -25,8 +27,8 @@ against different profiles, including the base RO-Crate profile and various exte - Supports [CLI-based validation](#cli-based-validation) as well as [programmatic validation](#programmatic-validation) (so it can easily be used by Python code). - Extensible framework: new RO-Crate profiles can be added, implementing profile requirements as SHACL shapes and/or Python code. -
-Note: this software is still work in progress. Feel free to try it out, +
+Note: this software is still work in progress. Feel free to try it out, report positive and negative feedback. We also welcome contributions, but we suggest you send us a note (e.g., by opening an Issue) before starting to develop any code. The implementation of validation code for additional RO-Crate profiles would be particularly welcome.
@@ -118,6 +120,9 @@ where `` is the path to the RO-Crate you want to validate. Type `rocrate-validator --help` for more information. +For package modes, validation scope, and supported input combinations, see the +[package modes guide](https://rocrate-validator.readthedocs.io/en/latest/6_package_modes.html). + ## Programmatic Validation You can also integrate the package programmatically in your Python code. @@ -134,8 +139,10 @@ settings = services.ValidationSettings( # Set the path to the RO-Crate root directory rocrate_uri='/path/to/ro-crate', # Set the identifier of the RO-Crate profile to use for validation. - # If not set, the system will attempt to automatically determine the appropriate validation profile. - profile_identifier='ro-crate-1.1', + # If not set, the base RO-Crate profile is used, which defaults to `ro-crate-1.3`. + # Use `services.detect_profiles(settings)` to determine the profiles a crate declares + # conformance to, and pass one of them here to validate against it instead. + profile_identifier='ro-crate-1.3', # Set the requirement level for the validation requirement_severity=models.Severity.REQUIRED, ) @@ -154,11 +161,32 @@ else: print(f"Detected issue of severity {issue.severity.name} with check \"{issue.check.identifier}\": {issue.message}") ``` +For repeated validation against the same profile, reuse a `Validator`. Profile +artifacts are prepared on the first call (or explicitly with `prepare()`) while +crate data and results remain isolated per call: + +```python +validator = models.Validator(settings) +validator.prepare() # optional eager warm-up + +result_a = validator.validate("/path/to/ro-crate-a") +result_b = validator.validate("/path/to/ro-crate-b") + +# Invalidate preparation after changing profile files on disk. +validator.clear_prepared_profiles() +``` + +For ordinary crates, profile artifacts containing relative IRIs are prepared +once against an internal base and materialized for each crate public ID. A +metadata document whose explicit JSON-LD `@base` differs from its crate public +ID keeps a separate prepared plan. Calls on the same `Validator` are serialized; +use one instance per concurrent worker. + The following is a possible output: ```bash RO-Crate is invalid! -Detected issue of severity REQUIRED with check "ro-crate-1.1:root_entity_exists: The RO-Crate must contain a root entity. +Detected issue of severity REQUIRED with check "ro-crate-1.3:root_entity_exists: The RO-Crate must contain a root entity. ``` ## Running the tests diff --git a/docs/0_toc.rst b/docs/0_toc.rst index a1f299bb7..1c2da5dc7 100644 --- a/docs/0_toc.rst +++ b/docs/0_toc.rst @@ -20,17 +20,26 @@ 1_installation 2_usage_cli 3_usage_api + +.. toctree:: + :maxdepth: 5 + :caption: Using the Validator + 4_how_it_works + 6_package_modes + 12_validation_profiles 5_offline_mode + 10_api + examples/index .. toctree:: :maxdepth: 5 - :caption: Resources + :caption: Profile Development - 12_validation_profiles 11_writing_a_profile - 10_api - genindex + 13_optimizing_shacl_target_selection + 14_graph_transformers + 15_building_documentation .. toctree:: :maxdepth: 1 diff --git a/docs/10_api.rst b/docs/10_api.rst index 715d3ca41..591601322 100644 --- a/docs/10_api.rst +++ b/docs/10_api.rst @@ -15,14 +15,6 @@ .. _api: -.. toctree:: - :maxdepth: 5 - :caption: Getting Started - -.. toctree:: - :maxdepth: 5 - :caption: Resources - ################################################### API documentation ################################################### @@ -32,6 +24,7 @@ This section documents the ``rocrate_validator`` package's API. The for using them to validate RO-Crate metadata, and for interpreting the results. Below, you will find descriptions and usage examples for the core services, models, and other components provided by the package. +See the :ref:`Index ` for an alphabetical list of API entries. The Python API is structured in three main parts. @@ -75,7 +68,7 @@ The validator's core is built on the main set of classes shown in the following RO-Crate ----------------------------------------- -.. autoclass:: rocrate_validator.models.ROCrate +.. autoclass:: rocrate_validator.rocrate.ROCrate :members: Profiles, Requirements, and Checks @@ -104,6 +97,9 @@ Severity Validation ----------------------------------------- +.. autoclass:: rocrate_validator.models.Validator + :members: + .. autoclass:: rocrate_validator.models.ValidationSettings :members: @@ -116,6 +112,55 @@ Validation .. autoclass:: rocrate_validator.models.CheckIssue :members: +.. _api-composed-profile-identities: + +Composed profile check identities +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When a validation target uses ``prof:isProfileOf`` without an overlay, an +inherited check keeps the identifier and profile of the source that defines it. +When the target also declares ``validator:isRuleOverlayOf``, the API exposes two +identities for checks reused from the overlaid source: + +* the **source identity** identifies the physical implementation and remains + unchanged; and +* the **effective identity** is the profile and identifier used to report the + check in the current validation. + +For example, if an inherited check from +``example-profile-1.2`` has source identifier +``example-profile-1.2_``, an overlay targeting +``example-profile-1.3`` reports it as: + +.. code-block:: json + + { + "identifier": "example-profile-1.3_", + "profile": "example-profile-1.3", + "source_identifier": "example-profile-1.2_", + "source_profile": "example-profile-1.2" + } + +The ``CheckIssue`` properties ``identifier`` and ``profile_identifier`` return +the effective identity. ``source_profile_identifier`` returns the profile that +physically defines the check. The same distinction is present in +``CheckIssue.to_dict()`` / ``to_json()`` through the ``identifier``, +``profile``, ``source_identifier`` and ``source_profile`` keys. + +For a check implementation redefined locally by 1.3, ``source_identifier`` +remains the physical identifier assigned to the 1.3 check, while the effective +identifier may reuse the relative suffix of the replaced 1.2 check. This keeps +the logical check stable without mutating either ``RequirementCheck`` object. + +``RequirementCheck.identifier`` always refers to the source identity. Code +that needs the context-local overlay identity should use +``ValidationContext.effective_check_identifier(check)`` and +``ValidationContext.effective_check_profile(check)``. + +``RequirementCheckValidationEvent`` exposes the same distinction through +``effective_identifier`` / ``effective_profile_identifier`` and +``source_identifier`` / ``source_profile_identifier``. + .. autoclass:: rocrate_validator.events.Event :members: diff --git a/docs/11_writing_a_profile.rst b/docs/11_writing_a_profile.rst index 86bdc8d63..8cc07b79c 100644 --- a/docs/11_writing_a_profile.rst +++ b/docs/11_writing_a_profile.rst @@ -28,6 +28,11 @@ For complex validation, you may also need some knowledge of SPARQL, an RDF query language. You can learn about SPARQL in the tutorial `Using SPARQL to access Linked Open Data `_. +For guidance on SHACL Core targets, SPARQL target performance, inference and +rule ordering, see :ref:`efficient-shacl-targets`. + +For the rare case where validation must preserve information from the original +graph before an ontology mix-in or inference, see :ref:`graph-transformers`. All these tools are best learned through practice and examples, so when building a profile, it's encouraged to use the `other profiles `_ @@ -94,33 +99,377 @@ These instructions assume you are familiar with code development using Python an #. When your profile & tests are written, open a pull request to contribute it back to the repository! -Overriding inherited checks ---------------------------- - -When a profile inherits from another profile (via ``prof:isProfileOf`` / -``prof:isTransitiveProfileOf``), it automatically receives every check -declared by its ancestors. The validator additionally supports -**override-by-name**: a child profile can replace an inherited check by -declaring a new check with the **same name**. +Writing validation checks +------------------------- -This allows an extension profile to *redefine* the content of an inherited -check β€” for example, to make a constraint stricter or looser, change its -severity, or, as described in the next section, fully deactivate it. - -Override-by-name is enabled by default. It can be disabled via the -``allow_requirement_check_override`` validation setting (CLI / API), which -will raise an error on duplicate check names instead. +A validation profile can contain two kinds of executable checks. SHACL checks +describe constraints over the crate's RDF graph; Python checks implement +procedural logic for cases that are not conveniently expressed in SHACL. Both +types can be assigned a severity and can participate in profile inheritance, +overrides, overlays and deactivation. SHACL checks ^^^^^^^^^^^^ +A SHACL ``NodeShape`` or ``PropertyShape`` becomes a validation check. Its +``sh:name`` is the human-readable check name used when matching an inherited +check for an override. The shape's target and constraints determine what is +validated; ``sh:severity`` or the profile directory determines its severity. + +For example, this shape requires every ``schema:Dataset`` to have a ``name``: + +.. code-block:: turtle + + @prefix schema: . + @prefix sh: . + + + a sh:NodeShape ; + sh:targetClass schema:Dataset ; + sh:property [ + sh:path schema:name ; + sh:name "Root data entity has a name" ; + sh:minCount 1 ; + sh:severity sh:Violation ; + ] . + +Python checks +^^^^^^^^^^^^^ + +Package mode selects Attached or Detached validation rules. Validation scope +selects whether to check metadata and data files, or metadata only. See +:ref:`package_modes` for definitions, defaults, and supported input combinations. + +For a check that applies only to one package mode, use +``context.ro_crate.is_attached()`` or ``context.ro_crate.is_detached()``. Both +return ``False`` when the mode is unspecified, as with dictionary input in +automatic mode. Record a skip if your check requires a mode that was not selected. +Do not choose the mode yourself from Root identifiers, the presence of data +files, or the resources described in the metadata. + +With ``metadata_only=True``, keep metadata checks active and skip checks that +need access to the data files (the payload). Attached rules can apply to metadata +supplied as a file, URL, or dictionary even when those data files are unavailable. +Detached identifier rules also remain active in metadata-only validation. + +The dedicated Detached Data Entity check enforces its identifier restriction. +General identifier checks should check syntax without reporting the same +restriction again. + +Python checks are declared with the ``@check`` decorator in a requirement +module. The decorator's ``name`` and severity identify the check in the same +way as ``sh:name`` and severity identify a SHACL check. The function receives +the validation context and returns the check result or raises a validation +error according to the Python check API. + +For example, the following requirement defines a Python check with the same +kind of identity as the SHACL example above: + +.. code-block:: python + + from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext + from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement + + @requirement(name="Root metadata checks") + class RootMetadataChecks(PyFunctionCheck): + @check(name="Root data entity has a name", severity=Severity.REQUIRED) + def check_name(self, context: ValidationContext) -> CheckResultValue: + # A check can be skipped before inspecting the crate. + if context.settings.metadata_only: + context.record_skip(self, "metadata-only mode", "configured") + return CheckResult.SKIPPED + + root_entity = context.ro_crate.metadata.get_root_data_entity() + if root_entity is None: + # None is normalized to CheckResult.SKIPPED. Use it when the + # check has no applicable value to inspect; record_skip() is + # preferable when the caller can provide a reason. + return None + + if not root_entity.get_property("name"): + # A failed check normally records the extra_check and returns + # False. The check framework associates the issue with self. + context.result.add_issue("The root data entity has no name", self) + return False + + # True is normalized to CheckResult.PASSED. + return True + +The return annotation is ``CheckResultValue`` because the framework accepts +three equivalent forms: ``True``/``False`` are normalized to passed/failed, +``CheckResult.SKIPPED`` (or the other ``CheckResult`` members) expresses an +explicit status, and ``None`` is normalized to skipped. When a check fails, +call ``context.result.add_issue(...)`` before returning ``False`` so that the +result contains an actionable extra_check. When it is skipped, prefer +``context.record_skip(...)`` when a reason and category should be exposed to +API and CLI consumers. + +``ExtraCheck`` Python checks +~~~~~~~~~~~~~~~~~~~~~~~~ + +Use the ``extra_check=True`` argument to ``@check`` for advisory checks that +report suspicious patterns without deciding whether a crate conforms to its +profile. ``ExtraCheck`` checks are disabled by default and can be enabled by users +with ``--extra-checks`` or the +``ValidationSettings.extra_checks`` setting. A extra_check check +must record each finding with ``context.result.add_extra_check(...)``; do not +use ``add_issue(...)`` for an advisory finding, because issues contribute to +the conformance result. + +For example: + +.. code-block:: python + + @check(name="Possible missing entity link", extra_check=True) + def check_entity_link(self, context: ValidationContext) -> bool: + entity = ... # inspect the entity using the validation context + if suspicious_link(entity): + context.result.add_extra_check( + self, + "This value may refer to an entity that is not declared in the crate.", + violating_entity=entity.get("@id"), + violating_property="relatedLink", + ) + return True + +The return value still follows the regular Python check contract, but a +extra_check check's failed status does not fail conformance. Findings are +available from ``ValidationResult.extra_checks`` and are presented separately +from conformance issues. For the user-facing options and result fields, see +:ref:`cli-based-validation` and :ref:`programmatic-validation`. + +.. _profile-composition: + +Composing validation profiles +----------------------------- + +RO-Crate Validator provides four complementary mechanisms for composing the +rules of related profiles: + +#. **Ordinary inheritance** applies the checks of a source profile when + validating against a more specialized target profile. +#. A **check override** replaces one inherited implementation with a local one. +#. A **rule overlay** keeps reusing the source implementations but attributes + them to the target in validation output. +#. **Deactivation** explicitly suppresses an inherited check. + +These mechanisms can be combined, but each answers a different question: +which profiles participate, which implementation runs, how a check is +presented, or whether it runs at all. + +The examples below use ``example-profile-1.2`` as source profile ``A`` and +``example-profile-1.3`` as target profile ``B``. + +.. container:: profile-composition-table + + .. list-table:: Profile composition mechanisms + :class: profile-composition-mechanisms + :header-rows: 1 + :widths: 5 20 30 45 + + * - # + - Mechanism + - How it is selected + - Effect + * - 1 + - Ordinary inheritance + - ``B prof:isProfileOf A`` + - Applies the rules of both ``A`` and ``B`` while preserving their + source profiles and identifiers. + * - 2 + - Check override + - A check in ``B`` with the same ``(name, severity)`` as a check in + ``A`` + - Executes the implementation from ``B`` instead of the matching + implementation from ``A``. + * - 3 + - Rule overlay + - ``B prof:isProfileOf A`` plus ``B validator:isRuleOverlayOf A`` + - Presents rules reused from ``A`` with effective profile and + identifiers based on ``B``, while retaining their source provenance. + * - 4 + - Deactivation + - A local deactivation declaration in ``B`` + - Skips the matching inherited check from ``A``. + +Replacing a bundled profile through ``--extra-profiles-path`` is not profile +composition: it loads a local profile in place of the predefined profile. See +:doc:`12_validation_profiles` for that separate mechanism. + +Ordinary profile inheritance +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Suppose that target profile ``B`` declares ``B prof:isProfileOf A``. RO-Crate +Validator interprets this relationship as ordinary profile inheritance: ``B`` +is a specialization of ``A`` (in other words, ``B`` *is an* ``A`` in the +validator's conformance model). Consequently, validating a crate against ``B`` +means applying both the rules defined by ``B`` and those defined by ``A``. The +validator reuses ``A``'s SHACL and Python checks directly, without copying them +into ``B``'s directory. The reused rules, and therefore their identifiers, +remain attributed to profile ``A``. + +In the concrete example, most 1.2 rules still apply to 1.3, so profile 1.3 can +reuse them and define only its new or changed rules. Its ``profile.ttl`` +declares the inheritance relationship: + +.. code-block:: turtle + + @prefix prof: . + + + a prof:Profile ; + prof:hasToken "example-profile-1.3" ; + prof:isProfileOf . + +In this example, RO-Crate Validator treats +``example-profile-1.3 prof:isProfileOf example-profile-1.2`` as a validation +dependency. It does **not** copy the 1.2 directory into the 1.3 directory, nor +does it merge their descriptions into a single +:class:`~rocrate_validator.models.profile.Profile` object. When validating +against 1.3, the validator instead: + +#. resolves profile 1.2 and any ancestors it declares from the profiles + available to the current validation session; +#. loads 1.2 requirements from the 1.2 directory and 1.3 requirements from the + 1.3 directory; +#. runs those requirements against the same crate; and +#. keeps each requirement and check linked to the profile and source file that + defined it. + +Consequently, both profile directories must be available to the validator. The +1.3 directory only needs its own ``profile.ttl``, resources and new or changed +checks. Unchanged checks continue to be loaded from the 1.2 directory at +validation time. + +For example, suppose 1.2 defines these checks: + +* ``Metadata file is valid JSON``; +* ``Root data entity has a name``; and +* ``Root data entity has a license``. + +Suppose 1.3 adds a new ``Metadata uses the 1.3 JSON-LD context`` check. With +ordinary inheritance, a validation targeting 1.3 executes: + +* the new ``Metadata uses the 1.3 JSON-LD context`` check from 1.3; and +* the unchanged JSON, name and license checks directly from the 1.2 directory. + +The new context check and its identifier belong to profile 1.3. The reused +JSON, name and license checks and their identifiers remain attributed to +profile 1.2. No source file is copied and profile 1.2 is not modified. + +.. _profile-rule-overlays: + +Rule overlays +^^^^^^^^^^^^^ + +A rule overlay lets a target profile report inherited checks as part of its +own rule set without copying their implementations. It changes the effective +profile and identifiers used in validation output, not which checks are +selected or executed. Profile 1.3 declares 1.2 as both its parent and its +overlay source: + +.. code-block:: turtle + + @prefix prof: . + @prefix validator: . + + + a prof:Profile ; + prof:hasToken "example-profile-1.3" ; + prof:isProfileOf ; + validator:isRuleOverlayOf . + +.. note:: + + ``validator:isRuleOverlayOf`` is an RO-Crate Validator extension, not a + property defined by the W3C Profiles Vocabulary. + + +For example, the inherited ``Root data entity has a license`` check is still +implemented by the 1.2 object, but its validation issue and event use 1.3 as +the effective profile. Their source fields continue to identify profile 1.2, +so API consumers can distinguish where a check came from and how it is +reported. The underlying 1.2 object is not mutated. The corresponding result +and event fields are described in :ref:`api-composed-profile-identities`. + +When the local 1.3 context check overrides the inherited 1.2 context check, the +overlay also rebases the effective identifier. For example, +``example-profile-1.2_`` becomes +``example-profile-1.3_`` while the replacement's physical identifier +remains available as its source identifier. This preserves a stable relative +identity across profile versions. + +An overlay source must be a direct parent. It does not implicitly overlay that +parent's ancestors. + +If a target overlays more than one direct parent, those source profiles must +not define checks with the same ``(name, severity)`` pair. Otherwise, a local +check in the target could match more than one source check and the validator +could not determine which implementation it overrides. + +The source and target profiles remain separate: unchanged checks stay in the +source directory, and each check keeps using the ontology and ``sh:prefixes`` +resources from the profile that defined it. If a changed 1.3 term mapping +alters a check's meaning, redefine that check locally or provide mappings that +preserve its semantics. + +.. note:: + + **When to use a rule overlay** + + Use a rule overlay when: + + * the target represents a new version or close variant of the source + profile; + * most validation rules are semantically unchanged; and + * inherited rules should be reported as part of the target profile. + + If reused checks should remain visibly owned by 1.2, use ordinary + inheritance alone. Do not use an overlay to replace a bundled profile with + a local copy; use the separate ``--extra-profiles-path`` mechanism + described in :doc:`12_validation_profiles`. + +Overlay validation processes 1.3 checks before checks loaded from 1.2. In the +example, the 1.3 context check can reject incompatible metadata before an +inherited 1.2 check attempts to consume it during fail-fast validation. + +Overriding inherited checks +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Suppose 1.2 defines a ``Metadata uses the expected JSON-LD context`` check and +1.3 directly inherits 1.2 through ``prof:isProfileOf``. Profile 1.3 can replace +that check by declaring a local check with the same **name and severity**. This +is a **check override**: the validator executes the 1.3 implementation and +does not execute the matching 1.2 implementation. The mechanism works with +ordinary inheritance as well as rule overlays. + +The pair ``(name, severity)`` is the check identity. For example, if the 1.2 +context check has ``REQUIRED`` severity but the 1.3 check with the same name +has ``RECOMMENDED`` severity, no override occurs and both checks remain active. +To change severity intentionally, 1.3 must deactivate the original +``REQUIRED`` check and add a distinct ``RECOMMENDED`` check. + +An override is resolved against direct parents only. Suppose 1.2 inherits the +context check from a base profile and 1.3 only declares +``1.3 prof:isProfileOf 1.2``. A check declared locally by 1.3 cannot override +that base-profile check. Its defining base profile must also be listed as a +direct parent of 1.3. This restriction keeps override resolution deterministic +in multiple-inheritance hierarchies. + +Check override is enabled by default. If the API setting +``allow_requirement_check_override`` is set to ``False``, the matching 1.2 and +1.3 context checks cause an error instead of selecting the 1.3 implementation. + +Overriding SHACL checks +~~~~~~~~~~~~~~~~~~~~~~~ + Each SHACL ``NodeShape`` / ``PropertyShape`` becomes a check whose name is -its ``sh:name``. To override an inherited check, declare a shape in the -extension profile with the **same** ``sh:name`` as the inherited one: +its ``sh:name``. To override the inherited 1.2 check, declare a shape in 1.3 +with the **same** ``sh:name`` and severity: .. code-block:: turtle - # Parent profile + # Source profile A (example-profile-1.2) ro:ShapeC a sh:NodeShape ; sh:name "The Shape C" ; @@ -135,7 +484,7 @@ extension profile with the **same** ``sh:name`` as the inherited one: .. code-block:: turtle - # Extension profile β€” overrides the inherited PropertyShape by sh:name + # Target profile B (example-profile-1.3) -- same sh:name and severity ro:ShapeC a sh:NodeShape ; sh:name "The Shape C" ; @@ -146,65 +495,84 @@ extension profile with the **same** ``sh:name`` as the inherited one: sh:path rdf:type ; sh:minCount 1 ; sh:maxCount 1 ; - sh:message "Stricter override from extension profile" ; + sh:message "Stricter override from profile 1.3" ; ] . Both top-level shapes and ``PropertyShape`` entries nested inside a parent ``NodeShape`` (i.e., declared inline, without an absolute IRI) can be overridden this way. -Python checks -^^^^^^^^^^^^^ +Overriding Python checks +~~~~~~~~~~~~~~~~~~~~~~~~ Python checks declared via the ``@check`` decorator are matched by their -``name`` argument. To override an inherited Python check, declare a new -function with the same ``name`` in the extension profile: +``name`` and severity arguments. To override an inherited 1.2 Python check, +declare a new function with the same identity in profile 1.3: .. code-block:: python - # In the extension profile's checks module - from rocrate_validator.requirements.python import check - - @check(name="Check Metadata File Descriptor entity existence") - def overridden_check(self, ctx): - # New implementation that replaces the inherited one - ... + # In target profile B's checks module (example-profile-1.3) + from rocrate_validator.models import CheckResultValue, Severity, ValidationContext + from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement + + @requirement(name="Metadata context checks") + class MetadataContextChecks(PyFunctionCheck): + @check(name="Metadata uses the expected JSON-LD context", severity=Severity.REQUIRED) + def check_context(self, context: ValidationContext) -> CheckResultValue: + metadata = context.ro_crate.metadata.as_dict() + actual_context = metadata.get("@context") + expected_context = "https://w3id.org/ro/crate/1.3/context" + if actual_context != expected_context: + context.result.add_issue( + f"Expected JSON-LD context {expected_context!r}, got {actual_context!r}", + self, + ) + return False + return True + +The function keeps the inherited check's name and severity, so only its +implementation changes: profile 1.3 accepts the 1.3 context while the 1.2 +implementation is not executed. + +Rule overlays and check overrides are complementary, not interchangeable. An +overlay determines the effective profile and identifier used for an inherited +check; an override determines which implementation is executed when the target +redefines that check. Deactivating inherited checks ------------------------------ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -A child profile can also **fully deactivate** a check inherited from one of -its ancestors. A deactivated check is skipped during validation and -reported as such in the validation result. This is useful when an extension -profile relaxes the parent's expectations, or replaces a coarse-grained -check with a more specific one declared elsewhere in the same profile. +Suppose a 1.2 check no longer applies to 1.3, rather than merely requiring a +different implementation. Profile 1.3 can **fully deactivate** that inherited +check. The validator then skips it and records it as deactivated in the +validation result. This is useful when 1.3 relaxes a 1.2 expectation or +replaces a coarse-grained 1.2 check with more specific 1.3 checks. -SHACL checks -^^^^^^^^^^^^ +Deactivating SHACL checks +~~~~~~~~~~~~~~~~~~~~~~~~~ Two complementary mechanisms are supported, depending on whether the shape to disable has an absolute IRI of its own. **Shape with an absolute IRI** (e.g. a top-level ``NodeShape`` or a named -``PropertyShape``): reference the shape by IRI from the extension profile -and mark it as deactivated, without redeclaring it. +``PropertyShape``): profile 1.3 references the 1.2 shape by IRI and marks it as +deactivated, without redeclaring it. .. code-block:: turtle - # Extension profile - sh:deactivated true . + # Target profile B (example-profile-1.3) + sh:deactivated true . **Nested ``PropertyShape`` without an absolute IRI** (a property declared -inline inside a parent ``NodeShape``): use the override-by-name mechanism +inline inside a parent ``NodeShape``): use the check override mechanism described in the previous section. Declare a new ``PropertyShape`` in the -extension profile with the same ``sh:name`` as the one to disable, and set -``sh:deactivated true`` on it. This overrides the parent's -``PropertyShape``, and the validator reports the resulting check as -deactivated. +1.3 profile with the same identity as the one to disable, and set +``sh:deactivated true`` on it. This overrides the 1.2 ``PropertyShape``, and +the validator reports the resulting 1.3 check as deactivated. .. code-block:: turtle - # Extension profile β€” disables the inherited PropertyShape by sh:name + # Target profile B (example-profile-1.3) -- disables the inherited check ro:ShapeC a sh:NodeShape ; sh:name "The Shape C" ; @@ -218,30 +586,49 @@ deactivated. .. note:: - Cross-profile deactivation is scoped to the shape's transitive - descendants: a ``sh:deactivated true`` triple declared by a profile - that does not inherit (directly or transitively) from the shape's - owning profile is ignored. This prevents unrelated profiles loaded in - the same process from interfering with one another. + Profile 1.3 can deactivate the 1.2 shape because it is a descendant of 1.2. + If an unrelated profile ``C`` declares the same ``sh:deactivated true`` + triple but does not inherit from 1.2, the declaration is ignored. This + prevents unrelated profiles loaded in the same process from interfering + with one another. -Python checks -^^^^^^^^^^^^^ +Deactivating Python checks +~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``@check`` decorator accepts a ``deactivated`` flag, mirroring SHACL's -``sh:deactivated``. Combined with override-by-name, an extension profile -can disable an inherited Python check by redeclaring it with +``sh:deactivated``. Combined with check override, profile 1.3 can disable an +inherited 1.2 Python check by redeclaring it with ``deactivated=True``: .. code-block:: python from rocrate_validator.requirements.python import check + # In target profile B's checks module (example-profile-1.3) @check(name="Check Metadata File Descriptor entity existence", deactivated=True) def disabled(self, ctx): # Body is irrelevant β€” the check is skipped during validation. return True +Checking a profile during development +------------------------------------- + +Run the profile consistency checks before publishing a new or overlaid +profile: + +.. code-block:: bash + + rocrate-validator profiles check example-profile-1.3 + +The command checks the selected profile and its inherited profiles and reports +each consistency check as ``PASS`` or ``FAIL``. In particular, it detects +duplicate ``(name, severity)`` identities within a profile, invalid or unloaded +overlay sources, and ambiguous identities contributed by multiple overlay +sources. Validation runs perform these profile checks automatically; +``validate --no-profile-checks`` can disable that automatic step when +necessary. + Running validator & tests during profile development ---------------------------------------------------- diff --git a/docs/12_validation_profiles.rst b/docs/12_validation_profiles.rst index 4902050e8..260641e18 100644 --- a/docs/12_validation_profiles.rst +++ b/docs/12_validation_profiles.rst @@ -45,10 +45,27 @@ Behavior ^^^^^^^^ * Profiles provided via ``--extra-profiles-path`` are **loaded in addition to** the system’s predefined profiles. -* If an additional profile has the **same name** as a predefined profile, the additional profile **overrides** the predefined one. +* If an additional profile has the **same profile URI** as a predefined + profile, the additional profile **overrides** the predefined one. This mechanism allows you to: * **Add new custom validation profiles** to implement project-specific checks. * **Modify existing profiles** without altering the system’s predefined configuration files. * **Maintain a clear separation** between standard validation logic and project-specific customizations. + +.. note:: + + This is whole-profile replacement: a locally loaded profile takes the place + of a predefined profile with the same URI. It is different from a + :ref:`profile rule overlay `, which composes a target + profile from an inherited source profile and only redefines selected checks. + +.. seealso:: + + * To create a new profile or implement its checks, see + :doc:`Writing a profile <11_writing_a_profile>`. + * To load additional profiles from the command line or Python, see the + :doc:`CLI guide <2_usage_cli>` and the :doc:`Python API guide <3_usage_api>`. + * For profile-related services and models, see the + :doc:`API reference <10_api>`. diff --git a/docs/13_optimizing_shacl_target_selection.rst b/docs/13_optimizing_shacl_target_selection.rst new file mode 100644 index 000000000..8adb5fbc6 --- /dev/null +++ b/docs/13_optimizing_shacl_target_selection.rst @@ -0,0 +1,128 @@ +.. + Copyright (c) 2024-2026 CRS4 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +.. _efficient-shacl-targets: + +Optimizing SHACL target selection +================================= + +During rule expansion, pySHACL re-evaluates the targets of shapes that carry +rules as triples are added to the data graph (see the `SHACL-AF spec`_ for +details). Targets of validation-only shapes are evaluated afterwards, during +validation. A SPARQL target on a rule-bearing shape that is inexpensive on a +small test crate can therefore dominate runtime on a graph with thousands of +entities. + +.. _SHACL-AF spec: https://www.w3.org/TR/shacl-af/ + +Prefer SHACL Core targets such as ``sh:targetClass``, ``sh:targetNode``, +``sh:targetSubjectsOf`` and ``sh:targetObjectsOf`` when they express the +selection directly. An implicit class target is also available when a shape is +itself declared as an ``rdfs:Class``. Use ``sh:SPARQLTarget`` only when focus +nodes depend on a relationship or exclusion that Core targets cannot express. +Benchmark the complete profile either way: inference and iterative rules can +make a Core target process more focus nodes than an equivalent explicit query. + +Rules can classify entities for other shapes without a SPARQL target. All rules +are executed before validation begins, so a shape that merely validates the +inferred class needs no ``sh:order``: ordering matters only when one rule +depends on the output of another. Use ``sh:condition`` to restrict which +focus nodes a rule applies to. A validation-only consumer can then use +``sh:targetClass`` after rule expansion instead of paying for a SPARQL target +during the iterative rule phase. If the consuming shape also carries rules, +its target is still re-evaluated during expansion. + +Both shapes and rules have a default ``sh:order`` of zero, and rules sharing the +same order run in an unspecified sequence, so never rely on declaration order. +When a rule on shape A produces a type that shape B targets, assign shape A a +lower order than shape B so the type is inferred before B is evaluated. The +same principle applies to rules on the same shape: the producing rule must +precede the consuming one. Producers need a lower order than their consumers: + +.. code-block:: turtle + + ex:FindScripts a sh:NodeShape ; + sh:targetClass schema:SoftwareSourceCode ; + sh:order -1 ; + sh:rule [ + a sh:TripleRule ; + sh:condition [ + a sh:NodeShape ; + sh:not [ sh:class bioschemas:ComputationalWorkflow ] + ] ; + sh:subject sh:this ; + sh:predicate rdf:type ; + sh:object ex:Script ; + ] . + +.. note:: + + The SPARQL snippets in this section omit prefix boilerplate. A complete + ``sh:SPARQLTarget`` must make prefixes such as ``rdf:``, ``rdfs:``, + ``schema:`` and ``ro:`` available either through inline ``PREFIX`` + declarations or through ``sh:prefixes`` and ``sh:declare``. The bundled + profiles use the latter form. + +.. warning:: + + A ``sh:condition`` built on ``sh:not`` makes the rule non-monotonic. If + ``bioschemas:ComputationalWorkflow`` is itself inferred by another rule, the + rule above runs first and wrongly classifies the workflow as a script. + Iterative execution does not repair this, because an inferred triple is never + retracted. Whenever a condition is negative, the rule producing the negated + type must have a lower order than the rule testing for its absence. + +When Core targets cannot express the selection - for example, when focus nodes +depend on a relationship that ``sh:targetObjectsOf`` cannot capture, or when +an exclusion filter is required - a ``sh:SPARQLTarget`` is justified. In that +case, pre-compute expensive subpatterns in a subquery to avoid large +intermediate cross-products. For instance, when a target needs the Root Data +Entity, compute the small set of roots before joining it to entity patterns: + +.. code-block:: sparql + + SELECT ?this + WHERE { + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER(STRENDS(STR(?metadatafile), "ro-crate-metadata.json")) + } } + ?this a schema:MediaObject . + FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) + } + +.. note:: + + Without the distinct-root subquery, the target would have the following + form, with the entity and descriptor patterns together in the outer query: + + .. code-block:: sparql + + SELECT ?this + WHERE { + ?this a schema:MediaObject . + ?metadatafile schema:about ?root . + FILTER(STRENDS(STR(?metadatafile), "ro-crate-metadata.json")) + FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) + } + + For example, 500 ``schema:MediaObject`` entities and 250 entities with a + ``schema:about`` property, only one of which is the metadata descriptor, + produce 500 x 250, or 125,000, intermediate combinations before the filter + is applied. By contrast, the optimized query filters and deduplicates the + roots first, so the outer query processes only 500 combinations. On a + synthetic 750-triple graph using RDFLib 7.6.0, a local benchmark reduced + runtime from 13.82-14.58 seconds to 0.08 seconds + (timings are machine-dependent). diff --git a/docs/14_graph_transformers.rst b/docs/14_graph_transformers.rst new file mode 100644 index 000000000..d7f002931 --- /dev/null +++ b/docs/14_graph_transformers.rst @@ -0,0 +1,404 @@ +.. + Copyright (c) 2024-2026 CRS4 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +.. _graph-transformers: + +Graph transformers +================== + +Graph transformers are an internal preprocessing extension point for cases in +which a SHACL shape needs information that is available in the original +RO-Crate graph but would be lost or become ambiguous after an ontology mix-in, +inference, or SHACL rule expansion. They can also add a reusable annotation +which would otherwise have to be recomputed by several expensive SPARQL +targets. + +Do not use a transformer for an ordinary validation condition. Prefer SHACL +Core, a SPARQL constraint, or a Python check whenever those can express the +requirement directly. A transformer changes the graph seen by every SHACL +shape, so its output is global to the validation run and requires broader +regression testing. + +Why a single SHACL phase is not enough +-------------------------------------- + +Consider a crate containing a ``schema:PropertyValue`` whose +``schema:propertyID`` points to an identifier scheme. The scheme IRI is a +reference, not an entity described by the crate. The following is a complete +minimal JSON-LD input. The namespace ``https://vocab.example.org/`` is +an arbitrary external vocabulary namespace. + +.. code-block:: json + + { + "@context": { + "schema": "http://schema.org/", + "id": "@id", + "type": "@type", + "about": { "@id": "schema:about", "@type": "@id" }, + "identifier": { "@id": "schema:identifier", "@type": "@id" }, + "name": "schema:name", + "propertyID": { "@id": "schema:propertyID", "@type": "@id" }, + "value": "schema:value" + }, + "@graph": [ + { + "id": "ro-crate-metadata.json", + "type": "schema:CreativeWork", + "about": { "id": "./" } + }, + { + "id": "./", + "type": "schema:Dataset", + "name": "Example crate", + "identifier": { "id": "#identifier" } + }, + { + "id": "#identifier", + "type": "schema:PropertyValue", + "propertyID": { + "id": "https://vocab.example.org/identifier-scheme" + }, + "value": "ORCID" + } + ] + } + +After JSON-LD expansion, but **before an ontology mix-in, inference, or SHACL +rule expansion**, the relevant part of the RDF graph is: + +.. code-block:: turtle + + @prefix rdf: . + @prefix schema: . + @prefix vocab: . + + + rdf:type schema:CreativeWork ; + schema:about <./> . + + <./> + rdf:type schema:Dataset ; + schema:name "Example crate" ; + schema:identifier <#identifier> . + + <#identifier> + rdf:type schema:PropertyValue ; + schema:propertyID vocab:identifier-scheme ; + schema:value "ORCID" . + +At this point ``vocab:identifier-scheme`` occurs only as the object of +``schema:propertyID``. It has no outgoing assertion and is therefore a +cited-only reference. In particular, it is not yet a subject selected by a +target of the form ``?this a ?type``. + +A single-phase SHACL implementation can try to classify nodes from the triples +visible at the time of validation. In pySHACL, a ``CONSTRUCT`` query is used as +the body of a ``sh:SPARQLRule`` through ``sh:construct``; it is not the query +form of a ``sh:SPARQLTarget``. The following is a complete shapes-graph +fragment. Its target is deliberately selected only after the ontology makes the +term an ``owl:Class`` instance: + +.. code-block:: turtle + + @prefix ex: . + @prefix owl: . + @prefix rdf: . + @prefix sh: . + + ex:CandidateMarkerShape + a sh:NodeShape ; + sh:targetClass owl:Class ; + sh:rule [ + a sh:SPARQLRule ; + sh:construct """ + CONSTRUCT { + $this ex:validationCandidate true + } + WHERE { + $this ?predicate ?object . + FILTER(?predicate != rdf:type) + } + """ + ] . + +This is the same mechanism described in the `pySHACL Inference and Rules +documentation `_ and in +the normative `SHACL-AF SPARQL Rules specification +`_. It is enabled in pySHACL with +``advanced=True`` (or can be run explicitly with ``shacl_rules(...)``). + +On the original Turtle graph, this rule cannot classify +``vocab:identifier-scheme`` from its outgoing assertions because the IRI does +not occur as a subject. The distinction is available at this stage: the IRI is +only the object of ``schema:propertyID``. + +Now suppose the validator supplies the following ontology graph, or enables +inference which produces equivalent additional triples: + +.. code-block:: turtle + + @prefix owl: . + @prefix rdfs: . + @prefix vocab: . + + vocab:identifier-scheme + a owl:Class ; + rdfs:label "Identifier scheme" . + +The ontology mix-in adds these ontology triples to the data graph before pySHACL +applies SHACL rules. With OWL-RL inference, further triples such as +the following can also be present: + +.. code-block:: turtle + + vocab:identifier-scheme + + vocab:identifier-scheme . + +The term is now a subject with a type and with outgoing predicates that were +not asserted by the crate. The same single-phase rule now sees the injected +``rdfs:label`` or inferred ``owl:sameAs`` and marks +``vocab:identifier-scheme`` as a validation candidate. It cannot distinguish +those generated statements from assertions in the crate. The same problem can +occur without an ontology when inference or an earlier SHACL rule expands the +graph. + +Changing ``sh:order`` does not solve this problem. SHACL rule ordering applies +after the ontology mix-in and pre-inference, so it cannot restore the +provenance that was present in the first Turtle graph. The required operation +is two-phase: classify nodes using the original graph, then validate a copy of +that graph after normal ontology and inference expansion. The graph transformer +pipeline implements that separation; the private candidate marker added to +the transient copy is not inferred from ontology triples. + +How the pipeline works +---------------------- + +Shapes explicitly declare the transformers they require with +``validator:requiresGraphTransformer``. If there are no such declarations, the +validator delegates the original graph to pySHACL without copying or annotating +it. This keeps ordinary SHACL profiles isolated from the preprocessing +extension. + +When at least one transformer is requested, ``prepare_data_graph`` creates one +transient copy of the parsed RO-Crate graph, discovers the bundled modules below +``rocrate_validator.transformers``, and runs only the requested +transformers. The cached source graph is never passed to a transformer and +remains unchanged. RDFLib datasets are copied as datasets, including their +default and named graphs. + +Transformer-dependent validation rejects ``sparql_mode=True`` because a remote +endpoint cannot receive transient local annotations. It also rejects +``inplace=True``: mutating a caller-owned graph would leak private annotations. +Both options retain their normal pySHACL behaviour when no transformer is +requested. + +Lower ``order`` values run first. Transformers with the same order are sorted +by their fully qualified Python name, so their order is deterministic but +should not be used to encode an implicit dependency. Assign distinct orders +when one transformer consumes triples produced by another. + +Package structure +~~~~~~~~~~~~~~~~~ + +The engine belongs to the SHACL validation subsystem and is contained in +``rocrate_validator.requirements.shacl.transformers``: + +* ``base.py`` defines the abstract transformer contract; +* ``registry.py`` owns registration metadata and deterministic ordering; +* ``discovery.py`` imports bundled transformer modules once, with locking for + concurrent first use; +* ``pipeline.py`` copies data graphs without flattening datasets and executes + the requested transformer subset; +* ``preparation.py`` is the integration facade used by ``SHACLValidator``. It + reads shape dependencies, validates incompatible pySHACL options, prepares + private data and shapes graphs, and protects closed shapes; +* ``vocabulary.py`` owns the RDF predicate through which shapes request a + transformer. + +Concrete implementations live directly in the sibling catalog package +``rocrate_validator.transformers``. This mirrors the visibility of the bundled +``profiles`` directory while keeping executable transformer code separate from +profile resources and from the SHACL engine. + +``SHACLValidator`` calls ``prepare_validation_graphs`` and does not implement +transformer discovery or transformer-specific graph mutation itself. This +boundary keeps the feature reusable internally without turning it into a +pySHACL rule language or a third-party plugin API. + +Generic SHACL concerns, such as injecting the validator's default SPARQL +prefixes and wrapping pySHACL failures, remain in ``SHACLValidator``. + +Developing transformers +----------------------- + +The following sections describe the contract and development workflow for +adding a transformer to the bundled package. + +Transformer contract +~~~~~~~~~~~~~~~~~~~~ + +Every transformer must: + +* accept exactly one ``rdflib.Graph``; +* return an ``rdflib.Graph`` (returning ``None`` is an error); +* define a stable, globally unique RDF identifier; +* be deterministic for the same input graph; +* avoid network access and process-global mutable state; +* treat the graph as transient validator state, never as metadata to write back + to the RO-Crate. + +The discovery mechanism covers modules bundled in +``rocrate_validator.transformers``. It is not an entry-point system for +arbitrary profile-local or third-party plugins. + +Function transformers +~~~~~~~~~~~~~~~~~~~~~ + +Use ``@graph_transformer`` for a stateless transformation that is naturally +expressed as a function: + +.. code-block:: python + + from rdflib import Graph, URIRef + + from rocrate_validator.requirements.shacl.transformers import graph_transformer + + EXAMPLE_TRANSFORMER = URIRef( + "https://github.com/crs4/rocrate-validator/graph-transformers/example" + ) + EXAMPLE_ANNOTATION = URIRef( + "https://github.com/crs4/rocrate-validator/graph-transformers/exampleAnnotation" + ) + + + @graph_transformer( + identifier=EXAMPLE_TRANSFORMER, + order=50, + annotation_predicates=(EXAMPLE_ANNOTATION,), + ) + def add_example_annotations(data_graph: Graph) -> Graph: + # Add annotations to the transient graph. + ... + return data_graph + +The module does not need to be imported manually. It is imported by discovery +the first time ``prepare_data_graph`` runs. + +Class transformers +~~~~~~~~~~~~~~~~~~ + +Use a ``GraphTransformer`` subclass when the implementation benefits from +private helper methods or a clearer class-level contract: + +.. code-block:: python + + from rdflib import Graph, URIRef + + from rocrate_validator.requirements.shacl.transformers import GraphTransformer + + + class ExampleTransformer(GraphTransformer): + identifier = URIRef( + "https://github.com/crs4/rocrate-validator/graph-transformers/example" + ) + order = 60 + + def transform(self, data_graph: Graph) -> Graph: + ... + return data_graph + +Every concrete subclass is registered automatically. It must have a no-argument +constructor; a new instance is created for each pipeline execution so state +does not leak between validations. Abstract intermediate subclasses are not +registered. + +Requesting a transformer from shapes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A registered transformer is not executed merely because its module exists. +Every shape which relies on it declares the dependency explicitly: + +.. code-block:: turtle + + @prefix ex: . + @prefix sh: . + @prefix validator: . + @prefix validator-transformers: + . + + ex:ExampleShape + a sh:NodeShape ; + validator:requiresGraphTransformer + validator-transformers:example ; + # normal targets and constraints follow + . + +Dependencies are collected from the complete merged shapes graph. Repeating a +declaration is harmless, and inherited or additional profiles participate in +the same request set. An unknown identifier fails validation instead of being +silently ignored. + +Private marker predicates and prefixes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A transformer may annotate the transient graph with private predicates. Keep +these predicates in the validator-owned namespace +``https://github.com/crs4/rocrate-validator/graph-transformers/`` and define +their RDFLib vocabulary terms next to the concrete implementation in +``rocrate_validator.transformers``. These triples are implementation details: +they must not be serialized into an RO-Crate or presented as vocabulary terms +owned by the crate. + +Declare every produced predicate through the transformer's +``annotation_predicates`` metadata. Before validation, the orchestrator adds +those predicates to ``sh:ignoredProperties`` on a transient copy of every +``sh:closed true`` shape. This preserves closed-shape semantics without +mutating the source shapes graph. SPARQL constraints which enumerate arbitrary +predicates should still exclude the validator-owned namespace explicitly. + +When a SHACL query consumes a marker, declare the namespace in the profile's +shared ``sh:prefixes`` block. For example: + +.. code-block:: turtle + + sh:declare [ + sh:prefix "validator-transformers" ; + sh:namespace + "https://github.com/crs4/rocrate-validator/graph-transformers/"^^xsd:anyURI ; + ] . + +The query can then select only nodes annotated before pySHACL expands the +graph: + +.. code-block:: sparql + + ?this validator-transformers:validationCandidate true . + +Testing a transformer +~~~~~~~~~~~~~~~~~~~~~ + +Add unit tests for the transformation itself and integration tests for every +SHACL target which consumes its output. At minimum, verify that: + +* the source graph is unchanged; +* profiles which do not request the transformer are unchanged; +* consecutive transformers see their predecessors' output; +* ordering is explicit when transformers depend on each other; +* ontology and inference triples do not accidentally enter the intended scope; +* private predicates do not invalidate closed shapes; +* RDFLib datasets preserve default and named graph data; +* representative large graphs remain within the project's performance budget. diff --git a/docs/15_building_documentation.rst b/docs/15_building_documentation.rst new file mode 100644 index 000000000..f6b243799 --- /dev/null +++ b/docs/15_building_documentation.rst @@ -0,0 +1,46 @@ +Building and previewing the documentation +========================================= + +Install the documentation dependencies +-------------------------------------- + +From the repository root, install the documentation dependency group into the +project's virtual environment: + +.. code-block:: console + + poetry install --with docs + +The documentation uses the Shibuya theme, with a custom palette and styles for +both light and dark mode. The ``docs/Makefile`` uses Sphinx from ``.venv`` when +available, otherwise it uses the executable on ``PATH``. + +Build HTML +---------- + +.. code-block:: console + + make -C docs html + +Open ``docs/_build/html/index.html`` to read the generated documentation. + +Live preview +------------ + +.. code-block:: console + + make -C docs serve + +Open ``http://localhost:8000`` in your browser. The server listens on +``0.0.0.0``, so it is also accessible through the machine's network address. +Changes to documentation sources, configuration, and static files trigger an +automatic rebuild and reload the browser page. Stop the server with ``Ctrl+C``. + +To choose a different address or port: + +.. code-block:: console + + make -C docs serve SERVE_HOST=127.0.0.1 SERVE_PORT=8080 + +The preview server uses ``sphinx-autobuild``. You can override its executable +with ``SPHINXAUTO``, and the regular build executable with ``SPHINXBUILD``. diff --git a/docs/1_installation.rst b/docs/1_installation.rst index 6a476edf9..c05754086 100644 --- a/docs/1_installation.rst +++ b/docs/1_installation.rst @@ -28,5 +28,5 @@ Installation .. include:: ../README.md :parser: myst_parser.sphinx_ - :start-line: 33 - :end-line: 92 + :start-after: ## Installation + :end-before: ## CLI-based Validation diff --git a/docs/2_usage_cli.rst b/docs/2_usage_cli.rst index 62951f5b1..25a8ad171 100644 --- a/docs/2_usage_cli.rst +++ b/docs/2_usage_cli.rst @@ -14,6 +14,8 @@ limitations under the License. +.. _cli-based-validation: + CLI Validation ============== @@ -29,11 +31,70 @@ CLI Validation .. Include the README section for the CLI validation .. include:: ../README.md :parser: myst_parser.sphinx_ - :start-line: 93 - :end-line: 120 + :start-after: ## CLI-based Validation + :end-before: ## Programmatic Validation + + +``ExtraCheck`` Checks +----------------- + +``ExtraCheck`` checks are advisory checks that can report suspicious metadata +patterns without affecting whether an RO-Crate conforms to its profile. They +are disabled by default. To run all checks classified as extra_check, pass +``--extra-checks`` to ``validate``: + +.. code-block:: console + + rocrate-validator validate --extra-checks + +To omit one extra_check check while running the others, combine this option with +``--skip-checks`` and the check's fully qualified identifier. The validation +report lists extra_check checks and their findings separately from conformance +issues. .. seealso:: - To validate without network access and manage the HTTP cache from the - command line (the ``--offline`` and ``--no-cache`` flags and the ``cache`` - subcommand), see :ref:`offline_mode`. + * To validate without network access and manage the HTTP cache from the + command line, see :ref:`offline_mode`. + * For the equivalent Python workflow, see the :doc:`Python API guide + <3_usage_api>` and the :doc:`API reference <10_api>`. + * To select additional validation profiles, see + :doc:`Validation profiles <12_validation_profiles>`; to create a new + profile, see :doc:`Writing a profile <11_writing_a_profile>`. + * For choosing Attached/Detached rules, checking only metadata, and + supported inputs, see :ref:`package_modes`. + +Profile detection when ``conformsTo`` is a string +------------------------------------------------- + +Profile detection expects JSON-LD references, for example +``"conformsTo": {"@id": "https://w3id.org/ro/crate/1.2"}``. If a +``conformsTo`` value is instead a string, interactive validation can suggest +the matching profile and ask for confirmation before using it. For example, +``"conformsTo": "https://w3id.org/ro/crate/1.2"`` can suggest the +``ro-crate-1.2`` profile even though the value should be an ``{"@id": "..."}`` +object. The suggestion is offered only if: + +* No ``conformsTo`` value is already a valid entity reference. +* The only problem found during profile detection is that ``conformsTo`` is + written as a string. +* That URI identifies exactly one available validation profile. + +Declining returns to manual profile selection. A warning identifies the affected +metadata entity and shows the correct ``{"@id": "..."}`` form. The metadata is +not changed, so the validation report can still include an error for the +malformed value. + +In non-interactive mode, string suggestions are not accepted automatically. +When no usable profile reference is found, validation falls back to the base +``ro-crate`` profile. Specify ``--profile-identifier`` to choose a profile +explicitly, or correct ``conformsTo`` in the metadata descriptor or Root Data +Entity. Use ``--no-auto-profile`` to disable profile detection. + +Invalid JSON syntax +------------------- + +Profile auto-detection requires syntactically valid JSON. If the metadata +descriptor contains invalid JSON, auto-detection stops and the CLI reports +the parser's reason, line and column, and the affected source line with a +pointer to the error. Correct the JSON syntax and run validation again. diff --git a/docs/3_usage_api.rst b/docs/3_usage_api.rst index aa86c0c40..66efd4c84 100644 --- a/docs/3_usage_api.rst +++ b/docs/3_usage_api.rst @@ -27,14 +27,104 @@ Programmatic Validation .. include:: ../README.md :parser: myst_parser.sphinx_ - :start-line: 121 - :end-line: 162 + :start-after: ## Programmatic Validation + :end-before: ## Running the tests + + +``ExtraCheck``s +----------------- + +``ExtraCheck`` checks are advisory and do not affect the conformance result. They +are disabled by default; set ``extra_checks`` on +``ValidationSettings`` to run them: + +.. code-block:: python + + from rocrate_validator import models, services + + settings = services.ValidationSettings( + rocrate_uri="path/to/rocrate", + profile_identifier="ro-crate-1.3", + requirement_severity=models.Severity.REQUIRED, + extra_checks=True, + ) + result = services.validate(settings) + +The resulting ``ValidationResult`` exposes these reports separately through +``result.extra_checks``. ``ExtraCheck`` checks are also listed separately in +``result.statistics``. .. seealso:: - To resolve resources from a local cache or run validation without network - access (the ``offline`` / ``no_cache`` settings of ``ValidationSettings``), - see :ref:`offline_mode`. + * To configure the HTTP cache or run validation without network access, + see :ref:`offline_mode`. + * For command-line validation, see the :doc:`CLI guide <2_usage_cli>`. + * For the complete package API, see the :doc:`API reference <10_api>`. + * To select or write validation profiles, see + :doc:`Validation profiles <12_validation_profiles>` and + :doc:`Writing a profile <11_writing_a_profile>`. + * For package modes, scope, and accepted input combinations, see + :ref:`package_modes`. + +Profile Detection +----------------- + +``detect_profiles`` returns the available validation profiles identified by +``conformsTo`` values written as JSON-LD entity references, such as +``{"@id": "https://w3id.org/ro/crate/1.2"}``. A URI written as a plain string +does not select a profile through this function. + +Use ``detect_profiles_detailed`` to also find out why profile detection could +not use a metadata value, or which profile a string value may have intended. +It returns a ``ProfileDetectionResult`` with three fields: + +* ``profiles``: the same profiles returned by ``detect_profiles``. +* ``diagnostics``: explanations of missing, malformed, or unrecognized values. + For a string-valued ``conformsTo``, an ``INVALID_CONFORMS_TO`` diagnostic shows + the required ``{"@id": "..."}`` form. +* ``suggested_profile``: the profile the crate appears to request when + ``conformsTo`` contains a URI as a string instead of an ``{"@id": "..."}`` + object. The application can choose to validate against this profile despite + the incorrect metadata format. This field is ``None`` unless all of the + following conditions hold: + + * No ``conformsTo`` value is already a valid entity reference. + * Every problem reported in ``diagnostics`` is a ``conformsTo`` value written + as a string; there are no other profile-detection problems. + * All those strings contain the same URI. + * Exactly one available validation profile has that URI. + +For example, ``"conformsTo": "https://w3id.org/ro/crate/1.2"`` can produce +``suggested_profile`` identifying ``ro-crate-1.2``, but that profile is not added +to ``profiles``. The calling application decides whether to use the suggestion +for validation. Neither function changes the metadata or corrects its errors. + +Each diagnostic has a stable ``code`` as well as the affected property, +metadata location, original value, and explanation. Codes distinguish an +invalid reference (``INVALID_REFERENCE``), a malformed ``conformsTo`` value +(``INVALID_CONFORMS_TO``), a URI that does not identify an available profile +(``PROFILE_NOT_FOUND``), and missing profile hints (``NO_PROFILE_HINT``). + +.. code-block:: python + + from rocrate_validator.services import detect_profiles_detailed, validate + + settings = {"rocrate_uri": "/path/to/crate"} + detection = detect_profiles_detailed(settings) + for diagnostic in detection.diagnostics: + print(diagnostic.code, diagnostic.message) + + if detection.suggested_profile is not None: + # The application decides whether to accept the suggestion. + settings["profile_identifier"] = detection.suggested_profile.identifier + result = validate(settings) + +Programmatic detection does not prompt. If the +application does not set ``profile_identifier``, ``validate`` uses the profile +configured in ``ValidationSettings`` (or its default). Selecting a suggested +profile does not repair invalid metadata: ``conformsTo`` must still be +corrected to a JSON-LD reference, such as +``{"@id": "https://w3id.org/ro/crate/1.2"}``. Metadata-only Validation @@ -45,6 +135,10 @@ the library also supports metadata-only validation. This is useful when you want to ensure that the metadata conforms to the expected schema without checking the actual data files. +You can check only metadata using either Attached or Detached rules. See +:ref:`package_modes` for the meaning of these modes, their defaults, and examples +using a metadata file or URL. + To perform metadata-only validation, you can use the `validate_metadata_as_dict` from the `rocrate_validator.services` module. This function takes a dictionary representing the metadata and validates it against a given validation profile. @@ -68,6 +162,83 @@ representing the metadata and validates it against a given validation profile. # process the validation result as needed ... +To check an edited metadata dictionary against payload files in an Attached +crate directory, pass ``package_root`` to ``services.validate()``: + +.. code-block:: python + + from pathlib import Path + from rocrate_validator import services + from rocrate_validator.models.settings import ValidationSettings + + settings = ValidationSettings( + rocrate_uri=None, + metadata_dict=rocrate_metadata, + package_root=Path("./my-crate"), + profile_identifier="ro-crate-1.3", + packaging_mode="attached", + metadata_only=False, + ) + result = services.validate(settings) + +The dictionary supplies the metadata being validated, replacing the physical +metadata descriptor. The directory supplies only the payload files: +``ro-crate-metadata.json`` is not required on disk and is ignored if present. +The report records +``metadata_source="dictionary"`` and the selected ``package_root``. + +For a runnable notebook comparing package modes, validation scopes, and report +output with dictionary input, see :ref:`validation_examples`. + + +Warm validation with a reusable Validator +----------------------------------------- + +Applications that validate repeatedly against the same profile can keep a +single :class:`rocrate_validator.models.Validator` instance. The first call +prepares the selected profile chain, requirements, SHACL shapes, and ontology; +later calls reuse that preparation when the profile settings and RDF-base mode +match. Ordinary crates at different locations therefore share one preparation. + +.. code-block:: python + + from rocrate_validator.models import ValidationSettings, Validator + + settings = ValidationSettings( + rocrate_uri="/path/to/default-crate", + profile_identifier="my-custom-profile", + extra_profiles_path="/path/to/custom-profiles", + ) + validator = Validator(settings) + + # Optional: move profile preparation out of the first measured request. + validator.prepare() + + default_result = validator.validate() # existing API remains supported + crate_a_result = validator.validate("/path/to/crate-a") + crate_b_result = validator.validate("/path/to/crate-b") + +The per-call crate argument does not mutate ``settings.rocrate_uri``. Metadata +dictionaries can be supplied with ``validator.validate(metadata_dict=data)``; +``rocrate_uri`` and ``metadata_dict`` are mutually exclusive in one call. + +Every call creates a fresh data graph, result, statistics updates, and SHACL +execution context. The same validator serializes overlapping calls because its +subscriber and current-context state are instance-local; use separate validator +instances when validations must run concurrently. + +For metadata without an explicit JSON-LD ``@base`` distinct from the crate +public ID, relative IRIs in profile artifacts are compiled against a stable +internal base. Per-run copies of the SHACL and ontology graphs rebase only those +relative terms to the current crate, while structural shape identifiers and the +crate data graph remain unchanged. Metadata whose explicit ``@base`` differs +from its crate public ID causes the validator to keep a separate prepared plan +for that base combination, limiting plan reuse across crate IDs. Validation +behavior is unchanged. After editing profile files in a long-running process, +call +``validator.clear_prepared_profiles()`` or ``validator.prepare(refresh=True)`` +before the next validation. + Formatting Validation Results ----------------------------- diff --git a/docs/4_how_it_works.rst b/docs/4_how_it_works.rst index 7801eaed1..429e9ad10 100644 --- a/docs/4_how_it_works.rst +++ b/docs/4_how_it_works.rst @@ -36,25 +36,79 @@ Validation profiles can be related by inheritance -- i.e., where one validation profile extends another one. For instance, Workflow Testing RO-Crate extends Workflow RO-Crate. +Validation lifecycle and prepared profiles +------------------------------------------ + +An instance of :class:`rocrate_validator.models.Validator` keeps the profile +preparation state used by its validation runs. This state contains the resolved +profile chain and the profile objects which lazily own requirements and SHACL +shape registries. Repeated calls on the same validator can therefore reuse +parsed profile artifacts instead of loading them again. + +Crate-specific state is never part of the prepared profile state. Every run +still creates a new validation context, data graph, result, and SHACL execution +context, so inference, SHACL rules, violations, and skipped checks cannot leak +from one run into another. Validation statistics use the same prepared profile +instances as their owning validation context rather than loading a second copy. + +Profile artifacts can contain relative IRIs. For the ordinary case where the +effective JSON-LD base is the crate public ID, profiles are prepared once using +a stable internal base. Each SHACL run copies the much smaller prepared shapes +and ontology graphs, rebases their relative data terms to the current crate, +and preserves structural shape identifiers. The crate data graph itself is +never rewritten. This permits different crate locations to share preparation +without leaking their public IDs into one another. + +If the metadata declares an explicit JSON-LD ``@base`` that differs from the +crate public ID, relative identifiers in the metadata resolve against that +``@base`` rather than the crate root. Since profile artifacts may contain terms +from both bases, the validator keeps a separate prepared plan for this base +combination instead of rebasing it for reuse across crate IDs. Validation +behavior is unchanged; only cross-crate plan reuse is more limited. + + Validation profile selection ---------------------------- * **Automatic Profile Matching** (default): By default, `rocrate-validator` will attempt to select the correct validation - profiles for the input RO-Crate based on the `conformsTo` property of the Root Data Entity. + profiles for the input RO-Crate using valid `conformsTo` references in the + metadata descriptor and Root Data Entity. - If a precise match is found for the `conformsTo` property, that profile is selected for validation. - If no precise match is found: - - in **Interactive Mode:** (available only through the CLI) the system - will prompt the user to select a profile from the list of - profiles to which the RO-Crate conforms; + - in **Interactive Mode** (CLI only), the validator explains why it could + not select a profile automatically. If `conformsTo` contains a profile + URI as a string, it may ask whether to use the matching profile anyway. + The warning shows how to replace the string with an entity reference + such as `{"@id": "https://w3id.org/ro/crate/1.2"}`. + + This suggestion is offered only if all of the following are true: + + * No `conformsTo` value is already a valid entity reference. + * The only problems found during profile detection are `conformsTo` + values written as strings. + * All those strings contain the same URI. + * Exactly one available validation profile has that URI. + + Accepting the suggestion selects that profile for validation. Declining + it, or having no suggestion, opens the manual profile selection prompt. + + - in **Non-Interactive Mode**, the validator cannot ask for confirmation, + so it does not use a profile suggested by a string-valued `conformsTo`. + Use `--profile-identifier` to select that profile explicitly in the CLI. + If no profile is selected explicitly or detected from valid references, + validation uses the base `ro-crate` profile. + + Profiles detected from valid references are used in either mode. The base + `ro-crate` rules are also applied through profile inheritance. - - in **Non-Interactive Mode:** the system will validate against all profiles - to which the RO-Crate conforms, and for which validation rules are available. - In all cases, input RO-Crates are validated against the base `ro-crate` profile. + Selecting a profile only chooses which rules to run. It does not repair + metadata: a string-valued `conformsTo` remains malformed and can still + produce a validation issue. * **Profile Versions**: - It may happen that the RO-Crate profile version to which the input diff --git a/docs/5_offline_mode.rst b/docs/5_offline_mode.rst index b65e6831f..afbbd591a 100644 --- a/docs/5_offline_mode.rst +++ b/docs/5_offline_mode.rst @@ -100,11 +100,11 @@ Before going offline you can pre-fetch everything you will need with rocrate-validator cache warm --all-profiles # Warm only specific profiles - rocrate-validator cache warm -p ro-crate-1.1 -p workflow-ro-crate-1.0 + rocrate-validator cache warm -p ro-crate-1.3 -p workflow-ro-crate-1.0 # Also fetch and cache remote RO-Crates or arbitrary URLs rocrate-validator cache warm --crate https://example.org/crate.zip - rocrate-validator cache warm -u https://w3id.org/ro/crate/1.1/context + rocrate-validator cache warm -u https://w3id.org/ro/crate/1.3/context When invoked without any source option, ``cache warm`` defaults to warming all installed profiles. A summary table reports which URLs were cached, skipped or @@ -125,7 +125,7 @@ The same offline behaviour can be enabled programmatically through settings = services.ValidationSettings( rocrate_uri='/path/to/ro-crate', - profile_identifier='ro-crate-1.1', + profile_identifier='ro-crate-1.3', # Serve every HTTP request from the cache; uncached resources fail. offline=True, # Optional: use a dedicated cache directory (defaults to the user cache). diff --git a/docs/6_package_modes.rst b/docs/6_package_modes.rst new file mode 100644 index 000000000..f17beb585 --- /dev/null +++ b/docs/6_package_modes.rst @@ -0,0 +1,348 @@ +.. _package_modes: + +Package modes and validation scope +================================== + +RO-Crate metadata describes data files and their context. It can be supplied +as part of a package or as a separate metadata document. The distinction +between Attached and Detached RO-Crates determines which validation rules apply. + +An **Attached RO-Crate** has a root directory containing +``ro-crate-metadata.json`` and, optionally, data files. These data files are +called the **payload**. An Attached crate can also refer to resources on the web. + +A **Detached RO-Crate** is a metadata document used without a package root +directory. Its Data Entities (files and directories described in the metadata, +excluding the Root Data Entity, which describes the crate as a whole) must be +web-based resources with absolute identifiers. An absolute identifier includes +a URI scheme, as in ``https://example.org/data.csv``; ``data.csv`` is relative. + +Package mode and validation scope answer two separate questions: + +* **Which rules should be applied?** Use ``--packaging-mode`` to select + Attached or Detached rules, or let ``auto`` choose from the input. +* **Should data files be checked?** Add ``--metadata-only`` to check only the + metadata. This keeps the selected mode's metadata rules. + +For example, if you have metadata from an Attached crate but cannot share its +data files, use ``--packaging-mode attached --metadata-only``. + +In this guide, a **metadata document** means JSON/JSON-LD supplied as a local +file or through a URL. A **dictionary input** means metadata already loaded +into a Python dictionary and supplied through the API. For a dictionary input, +``package_root`` optionally names the local package directory that provides its +payload files. + +To try these options with your own metadata dictionary, see the +:ref:`example notebook `. It compares all three modes +and demonstrates both validation scopes with a local package directory. + +Modes and automatic assumptions +------------------------------- + +The `RO-Crate specification +`_ +allows software to assume a package mode from its input or process metadata +without selecting a package mode. The validator uses the defaults described +below. These assumptions select the validation rules; they do not establish +the original package type. + +The CLI option ``--packaging-mode`` and API setting +``ValidationSettings.packaging_mode`` accept the following values: + +.. list-table:: Available modes + :header-rows: 1 + :class: package-mode-table + :widths: 20 80 + + * - Mode + - Behaviour + * - ``auto`` (default) + - * **Use when** β†’ the input-based assumptions below suit your case. + * **Effect** β†’ select rules from the input context; a dictionary without + ``package_root`` is *Unspecified*, while a dictionary with it is + *Attached*. + * - ``attached`` + - * **Use when** β†’ you are validating an Attached crate, including a copy + of its metadata supplied without the data files. + * **Effect** β†’ apply Attached rules. Data Entities can use relative + payload paths or absolute identifiers for web-based resources. + * - ``detached`` + - * **Use when** β†’ you are validating metadata as a Detached crate. + * **Effect** β†’ apply Detached rules. Data Entities must have absolute + identifiers and refer to web-based resources. + +With ``auto``, the validator makes these assumptions: + +* **Directory or ZIP** β†’ *Attached*. +* **Metadata document**, supplied as a local file or URL β†’ *Detached*. +* **Python dictionary without a package directory** β†’ *Unspecified*. Common + metadata checks run, while checks requiring Attached or Detached rules are + recorded as skipped. +* **Python dictionary with ``package_root``** β†’ *Attached*. The dictionary + supplies the metadata graph and the local directory supplies the payload. + +Here, *Unspecified* means that no package type was selected. It is a reported +value, not a fourth option for ``--packaging-mode``. For a dictionary without +``package_root``, select ``attached`` or ``detached`` explicitly to enable those +mode-specific checks. + +.. note:: + + **A metadata document is not necessarily a Detached crate.** It may be a + copy of the metadata from an Attached crate. In that case, select + ``--packaging-mode attached --metadata-only`` explicitly. Using + ``--metadata-only`` alone does not change the automatic choice of Detached. + +The automatic choice uses the input format. It does not examine the Root Data +Entity's identifier or determine whether the described data files exist or are +on the web. An Attached package may contain no data files. + +What metadata-only changes +-------------------------- + +``--metadata-only`` (``metadata_only=True`` in the API) skips data-file checks. +It does **not** change the selected package type or relax its metadata rules. +For example, Detached mode still requires absolute Data Entity identifiers: +``data.csv`` does not meet that requirement, even if a base URL could resolve it +to an absolute URL. See the `File Data Entity requirements +`_. + +A successful metadata-only result says nothing about whether the data files +are present or valid. For *Unspecified* input, checks specific to Attached or +Detached crates are also skipped. The report lists the skipped checks. + +Input compatibility +------------------- + +The tables below show which inputs each mode accepts, for each validation +scope. A cell containing Attached, Detached, or Unspecified means validation can +start using that type; it does not mean the crate passes validation. +**Rejected** means you must change the input or settings before validation can +start. Supported BagIt packages (a packaging format that wraps files and +metadata) follow the directory or ZIP rows, depending on how they are supplied. + +Full validation requested +~~~~~~~~~~~~~~~~~~~~~~~~~ + +This table applies to CLI invocations without ``--metadata-only`` and calls to +``services.validate()`` with ``metadata_only=False``. + +.. list-table:: Mode used when metadata and data-file checks are requested + :header-rows: 1 + :class: package-mode-table + :widths: 28 24 24 24 + + * - Input + - ``auto`` + - ``attached`` + - ``detached`` + * - Local directory + - Attached + - Attached + - Rejected: select a metadata file or URL instead + * - Local or remote ZIP + - Attached + - Attached + - Rejected: select a metadata file or URL instead + * - Local ``ro-crate-metadata.json`` + - Detached + - Attached; parent directory becomes the package root + - Detached + * - Other local JSON/JSON-LD document + - Detached + - Rejected: supply the package directory/ZIP or its ``ro-crate-metadata.json`` + - Detached + * - Metadata document supplied through a URL + - Detached + - Rejected: supply a package directory/ZIP or use metadata-only validation + - Detached + * - ``metadata_dict`` without ``package_root`` (API) + - Unspecified; only metadata is checked + - Rejected for full validation; set ``metadata_only=True`` + - Detached; only metadata is checked + * - ``metadata_dict`` with local ``package_root`` (API) + - Attached + - Attached + - Rejected: a package directory cannot be Detached + +Without ``package_root``, a dictionary has no payload backend, so ``auto`` and +``detached`` inputs are automatically limited to metadata-only validation. For +full Attached validation, set ``package_root`` to a local directory containing +the payload files. The dictionary replaces the metadata descriptor: a physical +``ro-crate-metadata.json`` is not required and, if present, is not read. +Metadata checks use the supplied dictionary, including in metadata-only mode. +The report identifies the dictionary as the metadata source and includes the +package root. + +When ``metadata_only=True``, ``package_root`` still selects Attached rules, but +checks that read payload files are skipped. + +For full Attached validation, supply the package directory or supported archive. +You can also select the local ``ro-crate-metadata.json`` file with +``--packaging-mode attached``; the validator then uses the directory containing +that file as the package root. + +Metadata-only validation requested +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This table applies with ``--metadata-only`` or ``metadata_only=True``. Every +accepted combination checks metadata without checking its data files. + +.. list-table:: Mode used when only metadata is checked + :header-rows: 1 + :class: package-mode-table + :widths: 28 24 24 24 + + * - Input + - ``auto`` + - ``attached`` + - ``detached`` + * - Local directory + - Attached + - Attached + - Rejected: select a metadata file or URL instead + * - Local or remote ZIP + - Attached + - Attached + - Rejected: select a metadata file or URL instead + * - Local ``ro-crate-metadata.json`` + - Detached + - Attached; data files need not be accessible + - Detached + * - Other local JSON/JSON-LD document + - Detached + - Attached; the input file can have any name + - Detached + * - Metadata document supplied through a URL + - Detached + - Attached; data files need not be accessible + - Detached + * - ``metadata_dict`` without ``package_root`` (API) + - Unspecified + - Attached + - Detached + * - ``metadata_dict`` with local ``package_root`` (API) + - Attached + - Attached + - Rejected: a package directory cannot be Detached + +The name of the input file can differ from the metadata document's identifier +inside the JSON. Allowing any input filename does not relax the rules for that +identifier or for the rest of the metadata. + + +Examples +-------- + +Command line +~~~~~~~~~~~~ + +Validate an Attached package or a Detached document explicitly: + +.. code-block:: console + + rocrate-validator validate ./crate --packaging-mode attached + rocrate-validator validate ./dataset-ro-crate-metadata.json --packaging-mode detached + +Validate metadata from an Attached crate without exposing its payload: + +.. code-block:: console + + rocrate-validator validate ./metadata.json --packaging-mode attached --metadata-only + rocrate-validator validate https://example.org/ro-crate-metadata.json --packaging-mode attached --metadata-only + +Python API +~~~~~~~~~~ + +Select Attached rules when submitting metadata from an Attached crate: + +.. code-block:: python + + from rocrate_validator import services + + result = services.validate({ + "rocrate_uri": "https://example.org/ro-crate-metadata.json", + "profile_identifier": "ro-crate-1.3", + "packaging_mode": "attached", + "metadata_only": True, + }) + +For dictionary-only input, ``validate_metadata_as_dict()`` enables +metadata-only validation for you. To validate a modified dictionary against a +local Attached package's payload, use ``services.validate()`` with +``metadata_dict``, ``package_root``, and ``metadata_only=False``: + +.. code-block:: python + + from pathlib import Path + from rocrate_validator import services + from rocrate_validator.models.settings import ValidationSettings + + settings = ValidationSettings( + rocrate_uri=None, + metadata_dict=metadata, + package_root=Path("./my-crate"), + profile_identifier="ro-crate-1.3", + packaging_mode="attached", + metadata_only=False, + ) + result = services.validate(settings) + +The directory supplies payload files; ``metadata_dict`` supplies the metadata +being validated. No metadata descriptor is required on disk. If the directory +contains one, it is ignored for this validation. + +.. code-block:: python + + import json + from rocrate_validator import services + + with open("metadata.json", encoding="utf-8") as source: + metadata = json.load(source) + + result = services.validate_metadata_as_dict(metadata, settings={ + "profile_identifier": "ro-crate-1.3", + "packaging_mode": "attached", + }) + +Omitting ``packaging_mode`` for a dictionary without ``package_root`` leaves +the mode unspecified. Calling ``services.validate()`` with Attached dictionary +input and no ``package_root`` requires ``metadata_only=True`` explicitly. + +Reading the reports +------------------- + +The reported **RO-Crate package type** tells you which rules were selected: +Attached, Detached, or Unspecified. It does not certify the original package's +type. **Validation scope** tells you whether metadata-only or full validation +was requested. In Python, read the selected type through +``result.context.ro_crate.package_type``. + +JSON results include these fields under ``validation_settings``: + +.. list-table:: Fields describing the selected mode and scope + :header-rows: 1 + :class: package-mode-table + :widths: 35 65 + + * - Field + - Meaning + * - ``packaging_mode`` + - Requested mode: ``auto``, ``attached``, or ``detached``. + * - ``package_type`` + - Mode actually used: ``attached``, ``detached``, or ``unspecified``. + * - ``packaging_mode_explicit`` + - ``true`` if you selected ``attached`` or ``detached``; ``false`` for ``auto``. + * - ``metadata_only`` + - ``true`` if only metadata is checked, including when the API enables this + automatically for dictionary-only input. + * - ``metadata_source`` + - ``dictionary`` when checks use the supplied metadata dictionary; otherwise + ``package descriptor``. + * - ``package_root`` + - Local package directory used to access payload files for dictionary input. + +The result's ``skipped_check_details`` explains why checks were skipped. Read +those details alongside the issues and validation scope to understand the limits +of the result. diff --git a/docs/Makefile b/docs/Makefile index d4bb2cbb9..6b539a908 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,10 +3,20 @@ # You can set these variables from the command line, and also # from the environment for the first two. +.DEFAULT_GOAL := help + SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build +# Prefer the checkout's documentation dependencies; allow an explicit override. +SPHINXBUILD ?= $(if $(wildcard ../.venv/bin/sphinx-build),../.venv/bin/sphinx-build,sphinx-build) SOURCEDIR = . BUILDDIR = _build +SPHINXAUTO ?= $(if $(wildcard ../.venv/bin/sphinx-autobuild),../.venv/bin/sphinx-autobuild,sphinx-autobuild) +SERVE_HOST ?= 0.0.0.0 +SERVE_PORT ?= 8000 + +.PHONY: serve +serve: + @$(SPHINXAUTO) --host $(SERVE_HOST) --port $(SERVE_PORT) "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS) # Put it first so that "make" without argument is like "make help". help: diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 000000000..6d1696136 --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,453 @@ +/* Teal palette with darker text accents for contrast in light mode. */ +:root { + --font-mono: "JetBrainsMono Nerd Font", "JetBrains Mono", "Fira Code", + "Cascadia Code", "SFMono-Regular", Menlo, Monaco, Consolas, + "Liberation Mono", monospace; +} + +code, +kbd, +pre, +samp, +.sy-head-brand strong, +.product-name { + font-family: var(--font-mono); +} + +html.light[data-accent-color="teal"] { + --toc-emphasis: #4b5358; + --accent-2: #f2f9f9; + --accent-3: #e5f3f3; + --accent-4: #d3ebec; + --accent-8: #39989f; + --accent-9: #175a5f; + --accent-10: #094b4b; + --accent-11: #175a5f; + --accent-12: #003333; + --accent-a2: rgba(57, 152, 159, 0.06); + --accent-a3: rgba(57, 152, 159, 0.13); + --accent-a4: rgba(57, 152, 159, 0.2); + --accent-a9: #175a5f; + --accent-a11: #175a5f; + --accent-contrast: #fff; + --color-surface-accent: #c2e5e6; + --sy-c-heading: #133233; + --sy-c-bold: #094b4b; + --sy-c-link: #175a5f; + --sy-c-link-hover: #094b4b; + --sy-c-foot-background: #edf6f6; + --yue-c-link-1: #175a5f; + --yue-c-link-2: #094b4b; +} + +html.dark[data-accent-color="teal"] { + --toc-emphasis: #c2cbce; + --accent-2: #0d292c; + --accent-3: #12383c; + --accent-4: #164247; + --accent-8: #39989f; + --accent-9: #81d8d5; + --accent-10: #a3e4e2; + --accent-11: #81d8d5; + --accent-12: #d5f3f2; + --accent-a2: rgba(129, 216, 213, 0.06); + --accent-a3: rgba(129, 216, 213, 0.13); + --accent-a4: rgba(129, 216, 213, 0.2); + --accent-a9: #81d8d5; + --accent-a11: #81d8d5; + --accent-contrast: #003333; + --color-surface-accent: #164247; + --sy-c-background: #071f23; + --sy-c-background-dropback: rgba(7, 31, 35, 0.96); + --sy-c-surface: #123035; + --sy-c-text: #e0efef; + --sy-c-light: #a8c2c4; + --sy-c-heading: #c2e5e6; + --sy-c-bold: #edfafa; + --sy-c-border: #285052; + --sy-c-divider: #234549; + --sy-c-link: #81d8d5; + --sy-c-link-hover: #a3e4e2; + --sy-c-foot-background: #003333; + --yue-c-link-1: #81d8d5; + --yue-c-link-2: #a3e4e2; +} + +/* The README's status callout has a light fallback for GitHub; adapt it to + * the documentation theme's dark palette when dark mode is selected. + */ +html.dark .project-status-note { + background-color: #123035 !important; + border-left-color: #39989f !important; + color: #e0efef !important; +} + +/* Callouts use teal for information, orange/yellow for warnings and attention, + * brown for important details, and red for errors. Override each semantic type: + * the theme's callout backgrounds otherwise remain too light in dark mode. + */ +.admonition, +.admonition:is(.note, .tip, .hint, .seealso) { + --color-1: var(--accent-2); + --color-2: var(--accent-3); + --color-3: var(--accent-9); + --color-4: var(--accent-11); + color: var(--sy-c-text); + --yue-c-link-1: var(--color-4); + --yue-c-link-2: var(--color-4); + --yue-c-link-border: var(--color-3); + --yue-c-code-text: var(--color-4); + --yue-c-code-background: var(--color-2); +} + +/* Keep the icon badges above the title strip, which extends into their area. */ +.admonition::before { + z-index: 2; +} + +.admonition::after { + z-index: 3; +} + +.admonition > .admonition-title { + z-index: 1; + color: var(--color-4); + background-color: var(--color-2); +} + +.admonition:is(.warning, .caution, .admonition-todo) { + --color-1: #fff7ed; + --color-2: #ffedd5; + --color-3: #b45309; + --color-4: #7c2d12; +} + +.admonition.attention { + --color-1: #fffbeb; + --color-2: #fef3c7; + --color-3: #a16207; + --color-4: #713f12; +} + +.admonition.important { + --color-1: #faf4ef; + --color-2: #efdfcf; + --color-3: #8b5e3c; + --color-4: #65432a; +} + +.admonition:is(.danger, .error) { + --color-1: #fff1f2; + --color-2: #ffe4e6; + --color-3: #be123c; + --color-4: #881337; +} + +html.dark .admonition, +html.dark .admonition:is(.note, .tip, .hint, .seealso) { + --color-1: #123035; + --color-2: #164247; + --color-3: #39989f; + --color-4: #81d8d5; + color: #e0efef; +} + +html.dark .admonition:is(.warning, .caution, .admonition-todo) { + --color-1: #382817; + --color-2: #51371d; + --color-3: #fb923c; + --color-4: #fed7aa; + color: #e0efef; +} + +html.dark .admonition.attention { + --color-1: #332c14; + --color-2: #49401c; + --color-3: #fbbf24; + --color-4: #fde68a; + color: #e0efef; +} + +html.dark .admonition.important { + --color-1: #30251f; + --color-2: #47352a; + --color-3: #c89b72; + --color-4: #eed0b5; + color: #e0efef; +} + +html.dark .admonition:is(.danger, .error) { + --color-1: #381e28; + --color-2: #512635; + --color-3: #fb7185; + --color-4: #fecdd3; + color: #e0efef; +} + +html.dark .admonition::after { + background-color: #071f23; +} + +html.dark .admonition > .admonition-title { + color: var(--color-4); + background-color: var(--color-2); +} + +/* Deep petrol header in both colour modes. Local variables also + * keep the search field, navigation icons, and mobile menu readable. + */ +.sy-head { + color: #f5f5f5; + --sy-c-text: #f5f5f5; + --sy-c-light: #c2e5e6; + --sy-c-bold: #fff; + --sy-c-background: #003333; + --sy-c-background-dropback: rgba(0, 51, 51, 0.97); + --sy-c-surface: #0b4b4b; + --sy-c-border: #39989f; + --sy-c-divider: #21605f; + --sy-c-link: #81d8d5; + --sy-c-link-hover: #c2e5e6; +} + +.sy-head .searchbox input::placeholder { + color: #c2e5e6; + opacity: 1; +} + +.sy-head .searchbox kbd { + color: #c2e5e6; + background-color: #0b4b4b; + border-color: #39989f; +} + +/* Use the logo's blue sparingly to distinguish the quick links in the header. */ +.sy-head-links .nav-link-icon { + color: #37a5da; + margin-right: 0.4rem; +} + +.sy-head-links a:focus-visible, +.sy-head-links button:focus-visible { + outline: 2px solid #37a5da; + outline-offset: 3px; + border-radius: 4px; +} + +/* Use visible, theme-colored bullets and progressive indentation in the TOCs. */ +.yue .toctree-wrapper > ul { + padding-left: 1.25rem; +} + +.yue .toctree-wrapper > ul > li::marker { + color: var(--accent-9); + font-size: 1.05em; +} + +.yue .toctree-wrapper > p.caption { + color: var(--toc-emphasis); + font-weight: 700; +} + +.yue .toctree-wrapper > ul > li > a { + color: var(--toc-emphasis); + font-weight: 600; +} + +.yue .toctree-wrapper ul ul:not(:empty) { + list-style-type: circle; + margin-left: 0.3rem; + padding-left: 1rem; + border-left: 1px solid var(--sy-c-divider); +} + +.yue .toctree-wrapper ul ul a { + font-weight: 400; + font-style: italic; +} + +.globaltoc > ul { + padding-left: 1.25rem; + list-style-type: disc; +} + +.globaltoc li.toctree-l1 > a { + display: block; + padding: 0.18rem 0.4rem; + border-radius: 4px; + color: var(--toc-emphasis); + font-weight: 600; +} + +.globaltoc li.toctree-l2 > a, +.globaltoc li.toctree-l3 > a { + font-weight: 400; + font-style: italic; +} + +.globaltoc > ul > li::marker { + color: var(--accent-9); +} + +.globaltoc > ul > li.current > a { + font-weight: 700; +} + +.globaltoc li.current > a.current { + border-left: 3px solid var(--accent-9); + background-color: var(--accent-a2); + color: var(--toc-emphasis); +} + +/* Hide toggles for empty toctree placeholders; keep useful expand controls. */ +.globaltoc li:has(> ul.simple) > button { + display: none; +} + +.globaltoc ul ul { + padding-left: 1rem; + list-style-type: circle; +} + +.globaltoc ul ul ul { + list-style-type: square; +} + +.localtoc ul { + margin-left: 0.15rem; + padding-left: 0.9rem; + border-left: 1px solid var(--sy-c-divider); + list-style: none; +} + +.localtoc > ul { + margin-left: 0; +} + +.localtoc li { + position: relative; + padding-left: 0.55rem; +} + +.localtoc > ul > li > a { + color: var(--toc-emphasis); + font-weight: 600; +} + +.localtoc ul ul > li > a { + font-size: 0.96em; + font-weight: 400; + font-style: italic; +} + +.localtoc ul ul ul > li > a { + font-size: 0.92em; + font-style: normal; +} + +.localtoc ul ul ul ul > li > a { + font-size: 0.88em; + color: var(--sy-c-light); +} + +.localtoc li::before { + position: absolute; + top: 0.5em; + left: -1rem; + font-family: system-ui, sans-serif; + font-size: 0.55rem; + font-weight: 700; + line-height: 1; + color: var(--accent-8); + content: "●"; +} + +.localtoc ul ul { + margin-top: 0.25rem; + margin-bottom: 0.25rem; + padding-left: 0.75rem; +} + +.localtoc ul ul li::before { + top: 0.5em; + left: -0.82rem; + font-size: 0.48rem; + color: var(--sy-c-light); + content: "β—‹"; +} + +.localtoc ul ul ul li::before { + font-size: 0.42rem; + font-weight: 700; + color: var(--accent-8); + content: "β–ͺ"; +} + +.localtoc ul ul ul ul li::before { + font-size: 0.45rem; + font-weight: 400; + color: var(--sy-c-light); + content: "–"; +} + +.localtoc li.active > a { + color: var(--toc-emphasis); + font-weight: 700; + font-style: normal; +} + +.localtoc li.active::before { + color: var(--accent-9); +} + +/* Keep the composition summary within the documentation content column. */ +.profile-composition-table { + width: 100%; +} + +.profile-composition-table table.profile-composition-mechanisms { + table-layout: fixed; + width: 100%; +} + +.profile-composition-table th, +.profile-composition-table td { + overflow-wrap: break-word; + white-space: normal; +} + +.profile-composition-table th:first-child, +.profile-composition-table td:first-child { + text-align: center; +} + +/* Fit package-mode tables to the content column, including long API names. */ +table.docutils.package-mode-table { + table-layout: fixed; + width: 100%; + min-width: 0; +} + +table.docutils.package-mode-table th, +table.docutils.package-mode-table td, +table.docutils.package-mode-table caption { + white-space: normal; + overflow-wrap: anywhere; +} + +table.docutils.package-mode-table code { + white-space: normal; + overflow-wrap: anywhere; +} + +table.docutils.package-mode-table th, +table.docutils.package-mode-table td { + vertical-align: top; +} + +@media (max-width: 600px) { + table.docutils.package-mode-table th, + table.docutils.package-mode-table td { + padding: 0.4rem; + } +} diff --git a/docs/_templates/components/nav-links.html b/docs/_templates/components/nav-links.html new file mode 100644 index 000000000..6eadbced3 --- /dev/null +++ b/docs/_templates/components/nav-links.html @@ -0,0 +1,62 @@ +{%- macro expandURL(url, resource=False) -%} +{%- if url.startswith(('https://', 'http://')) -%} +{{ url }} +{%- else -%} +{{ pathto(url, resource=resource) }} +{%- endif -%} +{%- endmacro -%} + + diff --git a/docs/ack.rst b/docs/ack.rst index cc486da36..2e95aa310 100644 --- a/docs/ack.rst +++ b/docs/ack.rst @@ -20,4 +20,4 @@ Acknowledgements .. "## Acknowledgements" heading is skipped: the page title above replaces it). .. include:: ../README.md :parser: myst_parser.sphinx_ - :start-line: 182 + :start-after: ## Acknowledgements diff --git a/docs/conf.py b/docs/conf.py index 9f2896fe5..5a15af99f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,8 +28,15 @@ import sys from pathlib import Path +from pypandoc import get_pandoc_path + from rocrate_validator import __version__ +# nbconvert invokes Pandoc by name when rendering notebook Markdown. +# Make the executable bundled with the docs dependencies available on PATH. +pandoc_bin_dir = str(Path(get_pandoc_path()).resolve().parent) +os.environ["PATH"] = os.pathsep.join((os.environ.get("PATH", ""), pandoc_bin_dir)) + # update PYTHONPATH sys.path.insert(0, str(Path.cwd())) sys.path.insert(0, str(Path("..").resolve())) @@ -73,10 +80,9 @@ "sphinx_copybutton", ] -# Only auto-generate section labels for the top two heading levels: deeper -# subsections (e.g. the repeated "SHACL checks" / "Python checks" headings) -# would otherwise produce duplicate-label warnings within the same document. -autosectionlabel_maxdepth = 2 +# Leave autosectionlabel_maxdepth unset: nested sections imported from Markdown +# via the include directive's parser option retain a temporary document, which +# breaks Sphinx's section-depth calculation. # Warnings raised while embedding the Markdown README into the Sphinx pages. # The README is the canonical GitHub document: its slices intentionally start @@ -115,7 +121,11 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = "sphinx_rtd_theme" +html_theme = "shibuya" +html_title = "RO-Crate Validator" + +html_static_path = ["_static"] +html_css_files = ["custom.css"] autodoc_member_order = "bysource" @@ -132,10 +142,25 @@ branch = os.popen("git rev-parse --abbrev-ref HEAD").read().strip() # -- Options for HTML output ------------------------------------------------- +html_theme_options = { + "accent_color": "teal", + "nav_links": [ + {"title": "CLI", "url": "2_usage_cli", "icon": "lucide:terminal"}, + {"title": "API", "url": "3_usage_api", "icon": "lucide:braces"}, + { + "title": "Profiles", + "url": "12_validation_profiles", + "icon": "lucide:boxes", + }, + ], + "nav_socials": [ + {"name": "GitHub", "url": github_url, "icon": "simple-icons:github"}, + ], +} html_context = { - "display_github": True, # Integrate GitHub - "github_user": "crs4", # Username - "github_repo": "rocrate-validator", # Repo name - "github_version": branch, # Version - "conf_py_path": "/docs/", # Path in the checkout to the docs root + "source_type": "github", + "source_user": "crs4", + "source_repo": "rocrate-validator", + "source_version": branch, + "source_docs_path": "/docs/", } diff --git a/docs/examples/index.rst b/docs/examples/index.rst new file mode 100644 index 000000000..6521b6a75 --- /dev/null +++ b/docs/examples/index.rst @@ -0,0 +1,33 @@ +.. _validation_examples: + +Validation examples +=================== + +Validate a metadata dictionary +------------------------------ + +The notebook below lets you compare ``auto``, ``attached``, and ``detached`` +using the same metadata dictionary. It also demonstrates metadata-only and +full validation with a local package directory, and displays the report header +and JSON summary for each run. + +Read it online or :download:`download the notebook +` to run it locally. + +To run the examples: + +1. Open the notebook in a notebook editor, such as VS Code or Jupyter, with + the repository's ``.venv`` Python environment selected as the kernel. +2. Set ``INPUT_PATH`` to your metadata JSON file. For validation with payload + files, set ``ATTACHED_CRATE_DIR`` to your local crate directory. +3. Choose the target profile and ``OFFLINE`` setting, then run the cells in order. + External resources may be fetched when ``OFFLINE=False``. + +The documentation build does not execute the notebook. See +:doc:`../3_usage_api` for API usage and :ref:`package_modes` for the assumptions +and accepted input combinations. + +.. toctree:: + :maxdepth: 1 + + validate_metadata_dict_package_modes diff --git a/docs/examples/validate_metadata_dict_package_modes.ipynb b/docs/examples/validate_metadata_dict_package_modes.ipynb new file mode 100644 index 000000000..7b8cf203b --- /dev/null +++ b/docs/examples/validate_metadata_dict_package_modes.ipynb @@ -0,0 +1,261 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "df4ae258", + "metadata": {}, + "source": [ + "# Try RO-Crate metadata validation with a Python dictionary\n", + "\n", + "Set `INPUT_PATH` in the first setup cell to the JSON file you want to inspect. The notebook loads it into `INPUT` and validates that same dictionary with `packaging_mode` set to `auto`, `attached`, and `detached`.\n", + "\n", + "The first setup cell also contains the profile, offline-mode, and optional Attached crate directory parameters. Run this notebook from the repository checkout with its `.venv` kernel selected. Dictionary-only input has no payload backend, so validation is metadata-only. Full Attached validation is available when you also set `ATTACHED_CRATE_DIR` as the `package_root`.\n", + "\n", + "This notebook is also published in the documentation. Download it to run the examples locally; building the documentation does not execute validations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e0cd4479", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "from pathlib import Path\n", + "\n", + "from rocrate_validator.models.settings import ValidationSettings\n", + "from rocrate_validator.services import validate\n", + "from rocrate_validator.utils.io_helpers.output.console import Console\n", + "from rocrate_validator.utils.io_helpers.output.text.layout.report import ValidationReportLayout\n", + "\n", + "# Parameters: edit these values to try another metadata file or profile.\n", + "INPUT_PATH = Path(\"/tmp/mymetadata.json\")\n", + "PROFILE_IDENTIFIER = \"ro-crate-1.3\"\n", + "OFFLINE = False\n", + "ATTACHED_CRATE_DIR = Path(\"/tmp/profile-crate-process-run-crate/\") # Set this to a local crate directory\n", + "\n", + "with INPUT_PATH.open(encoding=\"utf-8\") as stream:\n", + " INPUT = json.load(stream)\n", + "\n", + "if not isinstance(INPUT, dict):\n", + " raise TypeError(f\"Expected a JSON object, got {type(INPUT).__name__}\")\n", + "\n", + "print(f\"Loaded {INPUT_PATH}: {len(INPUT.get('@graph', []))} graph entities\")\n", + "print(\"Top-level keys:\", list(INPUT))\n" + ] + }, + { + "cell_type": "markdown", + "id": "c48fa27f", + "metadata": {}, + "source": [ + "## Run one validation\n", + "\n", + "Use the parameters in the first setup cell to change the input file, target profile, or offline behavior. `OFFLINE=False` allows uncached JSON-LD contexts or external resources to be fetched; set it to `True` to use the validator cache only.\n", + "\n", + "Each run displays the same report header used by the CLI, followed by a JSON summary. With `package_root`, the `RO-Crate` row shows the supplied directory; dictionary-only input shows `in-memory metadata`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b7210d90", + "metadata": {}, + "outputs": [], + "source": [ + "def run_validation(package_mode=\"auto\", metadata_only=True, package_root=None):\n", + " settings = ValidationSettings(\n", + " rocrate_uri=None, # Not needed for dictionary input\n", + " metadata_dict=INPUT,\n", + " package_root=package_root,\n", + " profile_identifier=PROFILE_IDENTIFIER,\n", + " packaging_mode=package_mode,\n", + " metadata_only=metadata_only,\n", + " offline=OFFLINE,\n", + " )\n", + " result = validate(settings)\n", + " report = result.to_dict()\n", + " report_settings = report[\"validation_settings\"]\n", + " summary = {\n", + " \"requested_mode\": package_mode,\n", + " \"resolved_package_type\": report_settings[\"package_type\"],\n", + " \"metadata_source\": report_settings[\"metadata_source\"],\n", + " \"metadata_only\": report_settings[\"metadata_only\"],\n", + " \"package_root\": report_settings.get(\"package_root\"),\n", + " \"passed\": report[\"passed\"],\n", + " \"issues\": len(report[\"issues\"]),\n", + " \"skipped_checks\": report[\"skipped_checks\"],\n", + " }\n", + " console = Console(force_jupyter=True)\n", + " layout = ValidationReportLayout(console, result.validation_settings)\n", + " layout.result = result\n", + " _ = layout.layout\n", + " if layout.base_info_layout is not None:\n", + " console.print(layout.base_info_layout.renderable)\n", + " print(json.dumps(summary, indent=2))\n", + " return result, report\n", + "\n", + "result, report = run_validation(package_mode=\"auto\", metadata_only=True)\n" + ] + }, + { + "cell_type": "markdown", + "id": "0b48fb9b", + "metadata": {}, + "source": [ + "## Compare package modes\n", + "\n", + "Each run uses the same `INPUT` dictionary. `auto` resolves dictionary input to `UNSPECIFIED`; `attached` and `detached` select their respective profile rules. The report includes the resolved package type, validation scope, issues, and skipped checks.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28c7884f", + "metadata": {}, + "outputs": [], + "source": [ + "mode_results = {}\n", + "for mode in (\"auto\", \"attached\", \"detached\"):\n", + " print(f\"\\n=== packaging_mode={mode} ===\")\n", + " mode_results[mode] = run_validation(package_mode=mode, metadata_only=True)\n" + ] + }, + { + "cell_type": "markdown", + "id": "e20d3f53", + "metadata": {}, + "source": [ + "## Explore the `metadata_only` option\n", + "\n", + "A dictionary has no associated payload files. For `auto` and `detached`, the validator therefore makes the dictionary validation metadata-only even if `metadata_only=False` was requested. For explicit `attached`, `metadata_only=False` is rejected: provide `package_root` pointing to a local crate directory to perform full Attached validation with the dictionary.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42278547", + "metadata": {}, + "outputs": [], + "source": [ + "scope_results = {}\n", + "for mode in (\"auto\", \"detached\"):\n", + " print(f\"\\n=== packaging_mode={mode}, metadata_only=False ===\")\n", + " scope_results[(mode, False)] = run_validation(package_mode=mode, metadata_only=False)\n", + "\n", + "try:\n", + " run_validation(package_mode=\"attached\", metadata_only=False)\n", + "except ValueError as exc:\n", + " print(\"Expected configuration error for Attached dictionary input:\")\n", + " print(exc)\n" + ] + }, + { + "cell_type": "markdown", + "id": "216d377b", + "metadata": {}, + "source": [ + "## Dictionary plus attached directory with full validation\n", + "\n", + "This combination is supported: the dictionary supplies the metadata graph and `ATTACHED_CRATE_DIR` supplies the payload files. The dictionary replaces the metadata descriptor: `ro-crate-metadata.json` is not required on disk and is ignored if present. Metadata checks use `INPUT` in both validation scopes. The report records `metadata_source=\"dictionary\"` and the package root.\n", + "\n", + "Set `ATTACHED_CRATE_DIR` in the first setup cell to an existing local RO-Crate directory before running this section. `auto` resolves this input to Attached; the cell runs both `auto` and explicit `attached`, each with metadata-only and full validation. In every case, the `RO-Crate` row should show `ATTACHED_CRATE_DIR`. Results are saved in `package_results`, indexed by `(mode, metadata_only)`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "742f3d55", + "metadata": {}, + "outputs": [], + "source": [ + "package_results = {}\n", + "if not ATTACHED_CRATE_DIR.is_dir():\n", + " print(f\"Set ATTACHED_CRATE_DIR to an existing crate directory first: {ATTACHED_CRATE_DIR}\")\n", + "else:\n", + " for mode in (\"auto\", \"attached\"):\n", + " for metadata_only in (True, False):\n", + " print(f\"\\n=== dictionary + package_root, packaging_mode={mode}, metadata_only={metadata_only} ===\")\n", + " package_results[(mode, metadata_only)] = run_validation(\n", + " package_mode=mode,\n", + " metadata_only=metadata_only,\n", + " package_root=ATTACHED_CRATE_DIR,\n", + " )\n", + "\n", + " print(\"\\nDetached mode with package_root is an invalid combination:\")\n", + " try:\n", + " run_validation(\n", + " package_mode=\"detached\",\n", + " metadata_only=False,\n", + " package_root=ATTACHED_CRATE_DIR,\n", + " )\n", + " except ValueError as exc:\n", + " print(exc)\n" + ] + }, + { + "cell_type": "markdown", + "id": "846ede7b", + "metadata": {}, + "source": [ + "## Inspect findings\n", + "\n", + "Print issue summaries for one mode. Select `auto`, `attached`, or `detached` below.\n", + "\n", + "Set `INSPECT_PACKAGE_ROOT=True` to inspect a dictionary-plus-directory result from the preceding section; `SCOPE_TO_INSPECT` selects metadata-only (`True`) or full validation (`False`).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a890ec63", + "metadata": {}, + "outputs": [], + "source": [ + "MODE_TO_INSPECT = \"attached\"\n", + "INSPECT_PACKAGE_ROOT = False\n", + "SCOPE_TO_INSPECT = False\n", + "if INSPECT_PACKAGE_ROOT:\n", + " key = (MODE_TO_INSPECT, SCOPE_TO_INSPECT)\n", + " if key not in package_results:\n", + " raise ValueError(\"Run the dictionary-plus-directory section first; select auto or attached.\")\n", + " selected_result, selected_report = package_results[key]\n", + "else:\n", + " selected_result, selected_report = mode_results[MODE_TO_INSPECT]\n", + "print(f\"\\n=== inspecting mode: {MODE_TO_INSPECT} ===\\n\")\n", + "for issue in selected_report[\"issues\"]:\n", + " check = issue.get(\"check\", {})\n", + " print(f\"[{issue['severity']}] {check.get('identifier', '(unknown check)')}: {issue['message']}\")\n", + "\n", + "print(f\"\\nSkipped checks: {selected_report['skipped_checks']}\")\n", + "for skipped in selected_report.get(\"skipped_check_details\", []):\n", + " print(skipped)\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.13" + }, + "nbsphinx": { + "execute": "never" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/index.rst b/docs/index.rst index 9983d1ad6..0a9bd839b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,16 +13,18 @@ See the License for the specific language governing permissions and limitations under the License. -============================================= -Welcome to rocrate-validator's documentation! -============================================= +.. role:: product-name + +============================================================= +Welcome to :product-name:`rocrate-validator`'s documentation! +============================================================= .. _index: .. include:: ../README.md :parser: myst_parser.sphinx_ - :start-line: 2 - :end-line: 32 + :start-after: # rocrate-validator + :end-before: ## Installation .. include:: 0_toc.rst diff --git a/docs/requirements.txt b/docs/requirements.txt index 8d92b0be1..b46a0c9ac 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,8 +1,10 @@ # Required packages for building the documentation with Sphinx sphinx==8.1.3 nbsphinx==0.9.5 +pypandoc-binary==1.17 enum-tools==0.12.0 sphinx-toolbox==3.8.1 myst-parser==4.0.0 -sphinx_rtd_theme==3 +shibuya==2026.7.12 sphinx-copybutton==0.5.2 +sphinx-autobuild==2024.10.3 diff --git a/poetry.lock b/poetry.lock index eefd19f98..b953e6012 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.3 and should not be changed by hand. [[package]] name = "alabaster" @@ -12,6 +12,26 @@ files = [ {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, ] +[[package]] +name = "anyio" +version = "4.15.1" +description = "High-level concurrency and networking framework on top of asyncio or Trio" +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101"}, + {file = "anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +typing_extensions = {version = ">=4.16.0", markers = "python_version < \"3.15\""} + +[package.extras] +trio = ["trio (>=0.32.0)"] + [[package]] name = "apeye" version = "1.4.1" @@ -52,58 +72,82 @@ idna = ">=2.5" [[package]] name = "appnope" -version = "0.1.4" +version = "1.0.0" description = "Disable App Nap on macOS >= 10.9" optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" groups = ["dev"] markers = "platform_system == \"Darwin\"" files = [ - {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, - {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, + {file = "appnope-1.0.0-py3-none-any.whl", hash = "sha256:6fe0c04218aab65c54c4ff81638cdbf848d89f5653b74d68638a137f200dd16e"}, + {file = "appnope-1.0.0.tar.gz", hash = "sha256:685db59cb6043c3c2e528adc0b3bce3a5f8d09bcf7492c6ea650d1b7421f3c49"}, ] [[package]] name = "ast-serialize" -version = "0.5.0" +version = "0.13.1" description = "Python bindings for mypy AST serialization" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c"}, - {file = "ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb"}, - {file = "ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101"}, - {file = "ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a"}, - {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211"}, - {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf"}, - {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9"}, - {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee"}, - {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809"}, - {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43"}, - {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934"}, - {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759"}, - {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887"}, - {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27"}, - {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d"}, - {file = "ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a"}, - {file = "ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590"}, - {file = "ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642"}, - {file = "ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6"}, + {file = "ast_serialize-0.13.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3e129d44b0cffbea24a7f40117ce49e30993c3d119e584d4817ebd6f87246456"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:523d45bab16ddaf1399c04c4fe81bbf180f81e33c2f7bcbe76be56b0f7311ab4"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:638333fb8ae72c62753f9e794fda20290883ddc65b0d85e26e08f8bb227567ad"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc34c670747d85d4232834665e247009413eaebe47c69eaf60a5c439aaecaed9"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8c8ebd419811c435812d242fb2a173e57dfede198c9ac9a0a584666ebc9b9331"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8899c038b1c15ae8b98e379057fb3ef398ceb0b21d1737a8156dd5a0923b7e51"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e14c4de98f965647a74e6eaf37e20207124e45451ccd36e84a6f6ebab45e0372"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3e8a3eff69af640db985360fef35073fb46956dfc3e5c614cf00920d2746dd"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:75d7750fe98f6f35a96c43aba8d98a2e623e22c778a0c17ab9ede92eb10f5d82"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e8ddd63e80fc280c2f790157386f27f9b9f6ccb9c4f8e803b863cbfb085769"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9221e09496fb942901993c1812ee87950cc0ecbb514c7c0f1f5fa6d78f701ed8"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:7a5c38bdb93669c596f94fb7204dd6ebe4fadb19058ac9a2faf2948c2a175f96"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:acf4e9f055d75058ab01536da330648746f2f82b7697a85230547f2d5ebe895c"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87427913c325adc811e81e7c9a757b061f47ef1e29114ac479aa0953a2687a98"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:ad88921f95b92bc144d9ad059a0e16f8c1ae69f5239f2d3c6536fd2774f73dfe"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f8b17e2921e56621bcb46792bd3e0b82ca3b85de78d5936e7b8174ba1758b0aa"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-win32.whl", hash = "sha256:2f9a9ce8b0994f757b941f7f7fe384a6666b4162b941ebc5c248081baea18e47"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5d99910e6b9a9b9360bcca2ae99aee36cc912ce058df2a57d1c33099b6cb2593"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:a789cbe14d0a0e9ec9b32a456e5abc81ad8799479c3cfd88a29e3442262aebe6"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:23d768369b89d551077dc173da6892fad00443338417523b6732f4e8ba4a7ed5"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:68f8829d114dab6c9db4a8bbb8d0e342958faee1b5e15a29022678df639960f8"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e385b17d5bde892a6184003a9aff3518387908b73f8f5ea539313c07c8c0ede"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:02686b281ec9b8e9614f6f0db27e12e4cac515f336c5d05b678bc9eb73ff2af5"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8ab2c2585b2e22e48cd4aff2136fce4eab4ced569b85f150ef9a5a5d6f34d49"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c652d9f5d52d8b81eff3ff2c95c9dd0f281de45f6ad5d3fa95527550c396454a"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121e1d08d58b75c15491cc3003609cbad71ac2b0016043c36d30a6b2032b6605"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:30cf89b6a6844542d116c758725d13de5b97b9410a51ee577715f761270d0c1c"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:732aaae9e498c973fc63fd7f308009b57c4afa6ff59d0b674d894e57d0bca01a"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:d9c1a920b2aa0c1e016bed7721689717034ca0dba64ac3a3ff243b003d4e8dd8"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:847cad4c22953823f5e76d0b126c6f9da670cfa45b1df41b9324e22189d77611"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:aca07de3ca8e3d9f2b716c3c31f4f384fe4bd2d03e09912417cff7a3e08036f7"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:5db12d843d931b9d1f1519ce66b9cbfe72d8563b75c1204d255322254d1964dd"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:fb8021f4dc3296de18737e3b8b34994411da725374d74a0b9eb32a738f2a23d0"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:0d764913374ba21df8cd618045b371ed104ce55c6276de7c59e554a4f5ba26fd"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:2bad302ae2427454ef9dab71bd5c3ad67188860c39d36daee38f72031edb39c4"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:032b09d86c785c3984e975378b7861f2a68ac44e22ba7b07b93a536b2524eeb8"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:929eda0c419e622ab49df3a4c45f81c57dcc10874bbad55d0470a4a03324dac3"}, + {file = "ast_serialize-0.13.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bf53a3ba1cbe45399fc1e4ccd684b8f2a11cca450f89706b79122f478534c6f5"}, + {file = "ast_serialize-0.13.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4bcf1d5f4eefecaa325a34ecf079b2741189e7d0821523f92cb343a389a67489"}, + {file = "ast_serialize-0.13.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1b158f1c9e3a0d386913924c57995d6df78224868a4d0f215bdd41587c97b34a"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a6201b69ce42e919f2fd9a559138d5410b5d8afff7c3a6b1f500b0481ff7d8"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eb0be64c9b8f1ed26637eef345abe75ea90351c92c85a8aec7838b3af971011"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f2427bddfca208e84de700044fadf9aef2769a70bae740a1bdabb89ee9e1117"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88df960e67184a71874e5b2ee014f87d1254a0dbc1f903c7bd7b1ab7733ecaef"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f99786e0d62082a5b121b3437f3167562133711df22128de1e9886f57f0b226"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:a01b515b25a03184eff4acef25b981ae007916379682e908fc8785b342a0bf3e"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:29422d7bcd9e7f39411571c36cec45ce8a7a9b3ad4b2e7aa64497c15a9e397f6"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ffb0f548a7dc126067fdfb7b2325ab5dfcdeecc29faf5e0f0461c96d6ebfa3d"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2876444a5f87a7c76f371af3c07c2fda563a08bc8af52568df812e8aba4bf568"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:885a62ec6481ce58ab24c6357360d7a82023b62dd7e5e7ceccf59e9662a3272b"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:2015fd0165e463aac3b1aadc6a1fa1e5820c14ca950821f4fe02f8a3f6a388d6"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:eb41e1906502ac80d24ff343f45008552a3f5b5158607959fe6a07dbfc6eb037"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ca9acc67f796aa2554fa3037dccc022a4b08b38b2ad153edfac9c9bb7cb1f3c6"}, + {file = "ast_serialize-0.13.1-cp39-abi3-win32.whl", hash = "sha256:4f98da562e5901d69c7dd30885594c5438f859edf354e9ac7517d188fb026c3a"}, + {file = "ast_serialize-0.13.1-cp39-abi3-win_amd64.whl", hash = "sha256:ddf7cbc9c8abf70102c4a088efe154bb86bfb825b0a10e556d2b15972cf99bd7"}, + {file = "ast_serialize-0.13.1-cp39-abi3-win_arm64.whl", hash = "sha256:48f11d96c9ec71119612f8e79a8c55c12f2b136716d68347433c561d27a25f43"}, + {file = "ast_serialize-0.13.1.tar.gz", hash = "sha256:401cc84e8888d4d76f2c8d296fa006ed1a5ff993b6aff90dd777c03973efd025"}, ] [[package]] @@ -123,14 +167,14 @@ typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} [[package]] name = "asttokens" -version = "3.0.1" +version = "3.0.2" description = "Annotate AST trees with source code positions" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a"}, - {file = "asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7"}, + {file = "asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933"}, + {file = "asttokens-3.0.2.tar.gz", hash = "sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2"}, ] [package.extras] @@ -245,14 +289,14 @@ redis = ["redis (>=2.10.5)"] [[package]] name = "cattrs" -version = "26.1.0" +version = "26.2.1" description = "Composable complex class support for attrs and dataclasses." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "cattrs-26.1.0-py3-none-any.whl", hash = "sha256:d1e0804c42639494d469d08d4f26d6b9de9b8ab26b446db7b5f8c2e97f7c3096"}, - {file = "cattrs-26.1.0.tar.gz", hash = "sha256:fa239e0f0ec0715ba34852ce813986dfed1e12117e209b816ab87401271cdd40"}, + {file = "cattrs-26.2.1-py3-none-any.whl", hash = "sha256:a12aaa3453dc8f633a815293179f08b7421ed18d2575c459c3c736f840beac24"}, + {file = "cattrs-26.2.1.tar.gz", hash = "sha256:679132bfdc225c5ee40c024fc42519954767c387f950dc6751946c586bccdc6d"}, ] [package.dependencies] @@ -264,7 +308,7 @@ typing-extensions = ">=4.14.0" bson = ["pymongo (>=4.4.0)"] cbor2 = ["cbor2 (>=5.4.6)"] msgpack = ["msgpack (>=1.0.5)"] -msgspec = ["msgspec (>=0.19.0) ; implementation_name == \"cpython\""] +msgspec = ["msgspec (>=0.21.1) ; implementation_name == \"cpython\""] orjson = ["orjson (>=3.11.3) ; implementation_name == \"cpython\""] pyyaml = ["pyyaml (>=6.0)"] tomlkit = ["tomlkit (>=0.11.8)"] @@ -273,109 +317,125 @@ ujson = ["ujson (>=5.10.0)"] [[package]] name = "certifi" -version = "2026.6.17" +version = "2026.10.23" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main", "docs"] files = [ - {file = "certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db"}, - {file = "certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432"}, + {file = "certifi-2026.10.23-py3-none-any.whl", hash = "sha256:e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1"}, + {file = "certifi-2026.10.23.tar.gz", hash = "sha256:a9895e6e2758b371e0545e321a3a8fd1fcfd9829ada8824a936dfb16aaaf36cd"}, ] [[package]] name = "cffi" -version = "2.0.0" +version = "2.1.1" description = "Foreign Function Interface for Python calling C code." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev", "docs"] markers = "implementation_name == \"pypy\"" files = [ - {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, - {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, - {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, - {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, - {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, - {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, - {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, - {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, - {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, - {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, - {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, - {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, - {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, - {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, - {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, - {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, - {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, - {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, - {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, - {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, - {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, - {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, + {file = "cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be"}, + {file = "cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9"}, + {file = "cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41"}, + {file = "cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1"}, + {file = "cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12"}, + {file = "cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa"}, + {file = "cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3"}, + {file = "cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0"}, + {file = "cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455"}, + {file = "cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0"}, + {file = "cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf"}, + {file = "cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517"}, + {file = "cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735"}, + {file = "cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e"}, + {file = "cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a"}, + {file = "cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80"}, + {file = "cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e"}, + {file = "cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c"}, + {file = "cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6"}, + {file = "cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2"}, + {file = "cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b"}, + {file = "cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7"}, + {file = "cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac"}, + {file = "cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d"}, + {file = "cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973"}, + {file = "cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c"}, + {file = "cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb"}, + {file = "cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54"}, + {file = "cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96"}, + {file = "cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527"}, + {file = "cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13"}, + {file = "cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c"}, + {file = "cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48"}, + {file = "cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836"}, + {file = "cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3"}, + {file = "cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676"}, + {file = "cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e"}, + {file = "cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f"}, + {file = "cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4"}, + {file = "cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e"}, + {file = "cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5"}, + {file = "cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d"}, + {file = "cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b"}, + {file = "cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4"}, + {file = "cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399"}, + {file = "cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688"}, + {file = "cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7"}, + {file = "cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac"}, + {file = "cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960"}, + {file = "cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1"}, + {file = "cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc"}, + {file = "cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6"}, + {file = "cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94"}, + {file = "cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5"}, + {file = "cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66"}, + {file = "cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3"}, + {file = "cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692"}, + {file = "cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be"}, ] [package.dependencies] @@ -395,158 +455,198 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.4.7" +version = "3.5.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" groups = ["main", "docs"] files = [ - {file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943"}, - {file = "charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00"}, - {file = "charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6"}, - {file = "charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110"}, - {file = "charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f"}, - {file = "charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c"}, - {file = "charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-win32.whl", hash = "sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207"}, - {file = "charset_normalizer-3.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444"}, - {file = "charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c"}, - {file = "charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d"}, - {file = "charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:195c26fb65950f8fce54e26349852b7bdd7c5f120aeefbcc440b8a20faaed4a3"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9373ad13ef0d2c0fb761e04e55bfdee5a08b52cef2c882c8fbe9935b1517152e"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ddf19c062bea7a0cc80f519243d2c01dd091be0cf952a0750d4ad576709559f5"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d14b50de6bf4d0edf857a9386836846f982b8f524e188e2e68b96d702bcf4aa"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:28a15fdad492a99b6eccfaaed66ef3f74050680545ea61ec8b2f4c538f1f1320"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a893cc101149f80a653f82062ebc95b34525a2614382e1da5458fe7c6997249"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:619799369eeef6366ed3e8755a5670f4f2f0fb6b30a0fd7264dc0fdc2357058e"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:447441e76ec720b15e64418d32e092297340387053047c7c694f579efb0ee1d9"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:62588a277bfb59def052abd940703fa35107152bf479781a878617d60faf8fb5"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:44bd4fbb29dfbeba60e7d2bd000c59e4b21ddb3cc53912b14048d37092706d7c"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:30fcd120b732aa79317f08dee04d7de0847822e4cf7ee0e9f445bb958832252c"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:50e3adfb96fc189eb27b1cf62d3b598b89b4bb0420d93a3d3e42e137409011be"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b736353c0a625bbd5fcec108576e2385db3496f4f771f785ff32e108d3c3bc45"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-win32.whl", hash = "sha256:f5833ad231be5eb6553de524a70f48d71b2c8563101750531e0b80184e175cd4"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:1461ac396c4fdb983a675f20aa555624f0ee18ac83d832b9244ffff3d8055275"}, + {file = "charset_normalizer-3.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:c6708715abcf3c73b99508253e961a9967f02fe536532834149574eda6de0d1c"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d21b8b13c7592db2ac5e544a6d83187b995257472b0c9e8351b6d507ae37ed6"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d760fe2a4d7c3b226cb9026d6a842868d52a7901bd98420e1baf14e80da85cf5"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c9790464842f85f437dbbb54417eda1e0e6bfc52dd8d22d6fd1c994b73b2dc74"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4685902cf26edf013ed7a3da0f426ebba7a00ebb9541386d835afbf002c11cab"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4495c5002a7b28557e7e222e77e0b661183e432b7d6d2e788101e3f240e05b8c"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:211d5a3eb6af8f513b8d4ca19a8c1b7accab1b5f0d3175f9826b03c1a920dc1f"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef4fcbf3327382cd4c9f540babd61248208af7b93eec4de397b4d5f58a09e288"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd16aabe4a02a297c23417aa17ac6299dbd8c49f673bcd645b4929b11f5a4400"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:fb9e68df06293761f9fe66ade60a9bc6d0f5e42b8acf2939a9158af86ab0e5bd"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:59f63901b0031c3136cf64704dcb21de0bbae62ce2c9529bc39d27665463de37"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:304d5463e65a35d7bb0850550e0780395395f6fcf452f04db7d5ca7cecc425ac"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9cf9b1a857e25c4baceeb3624e92a56df3668f398c4acba74e174d81fb4d1d3a"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:114e4d0c92d618409ed82a99e22b5c5e768fe995f2973f78265f4524f49d4640"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-win32.whl", hash = "sha256:2625388c6c754520c37abaf3b41eb34d1cc4a373f457898f08606c8e362b891d"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:87e50a3e7cb90af586b6c5faf23e302a970415ac73bd7bd90a515a04b427ef96"}, + {file = "charset_normalizer-3.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:254eb48b9fa5ee9898a3c445825a1f340fe53712a098904b39b0bddba8ea3cb1"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed2a239c0ea213acc1908150a3037257083c7c083128f1a4cec2ec4b97dca491"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b91363207bd9dc966a691e959bb47f64b30f7ac4b072be9968b366982f7db77c"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:38a873987f3be698494da8b2e3085e29da02da7b633dce73e79c699a113d7bf0"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:355ad8011081dec5412240c087a9a0c9d4d5039f3ed11a3f13e18c2b29b56c51"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ee21e28f0430bd6dc9086c6e525d5e818a44a5ad19720c8a0ef766792f3eb5e5"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d31298449090ab8d47b7b1b2a555ff73cac7ed438a08b7ac160980c7ebed649"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5cde776b7cc66e4f6c99612cea4aa7269aa65863f7a15841b2c264f103822f4e"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae4f5fea5b8b8ccff88238cc8569303e5ee95efae67fa62922a311397a71f346"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f7d486c83842422badd511868fd8a9a20e9407ace71564b6af47ce7e60a336c1"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11a4d68a6ecda3292cb1e50239e111543ba5d709bb62a6b4ea1afcfa729d8875"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d6734d2ef8a50fbf8445c139477da401f50d62a0606bf00e20ec6d87773fefb1"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a815775b6c38d4e0ff7bcffbeba67feded90202bb6a226b8dd35f1c855217413"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:23851fb4e1b85ed3f6c2a27b777cdfe2e19fb5b38429a8faf38c7542b7665869"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-win32.whl", hash = "sha256:db19d07e2e0129e974a0e65d0064fc222a446cd5122c2fd4184d2af9fc734a9e"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:780fbe7cab297b81dad9fb8dc5eb003c0468ffb0d9e5f65068c53a34661a96bc"}, + {file = "charset_normalizer-3.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:e2af3aad578aa6bd1384bcf4750fc285e5a9de53f40b7d41e5a0bf748edeb2b3"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:ed905975ab14056a2e5eb1c376cb2e1ebc5396baf84163939c518556fccde9f5"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-android_24_x86_64.whl", hash = "sha256:a66c3bc5ab1f0ff2164fc9965ddd611ff0802173f4b9d24554c563f6ab7e1d6e"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2374b62878abb00cd8309b32af6c0b715cd02dec0ca74ef12e5069bdc64144a"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d376bbd28b3a8999db1a103b3b388aee6f1ddeb3e51bc2172993efdcd86e064d"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6045373d5a89a5ec71afde535db987ca28e76dfa276c2d4c818265b375d4b055"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:849df64e889b2e17230d58410a03dba311a65b163508fd33679b2b737d4b7858"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:15c44f7edfd477b06f517a5cc317fc1707edb9de2c865f43d4b6513907473234"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a89012d6d5476ee112d20d998570ed58df2260a852afb1758809cd6900411d21"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0c951d5e6dd9c2ff60609476752bee49da4206adde960ebc247766937f72e718"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7218e8f32b0956cfcd048fd42d9d5779809745ca1d86113ca56f66e7ae1549c4"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19a731138fc27d5682277d3b9df22855cea1239bce7fcec5f78f42ef2d1f3c3"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62603db9a7caa0802eaa28c1c46fecd7b3a263a774069c24c3c28c302448721c"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b6856554c4f44d79fc2307d5768854310a8f0096e501c75637542c82292b0429"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1bc0baf5ef96b6ede57d47f4b8fe4d9d84019c3bfcbeb20a41edc6a6ee341f1f"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:56bc200a365efb37383b7852e4cc5898d3b2da5987289b543956cf8cad71018a"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2c9ad19a6cfcd5ea5c0d41161d22f9df1dcc277e9bef2751391334546a314c00"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e243bd13217235fc7290c621941c3f5cc8b66e4872495be821d7436ba2fb838d"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:a090bb2c68df85450502e3e20d665e3a5af9c65a84d6508ed477badd49166fd3"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-win32.whl", hash = "sha256:2b7b3bbfb4fe8ef40600792d762fbaa9057559f9d3fad209525b7a22b99e91fd"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:78456a747de8dc58360ffa581f30a002baf5aa28cb262536545e91f113ed7639"}, + {file = "charset_normalizer-3.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:11912e4bb14baae7c5d8791aa55ba0a3a03ec6729073307b0f57270abaa713d3"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:1afb975bd5d68d5ce9f6b6d44fdf2f7e34b895a35e95708a7a91b20a3b51d187"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-android_24_x86_64.whl", hash = "sha256:bbbfc8e28816f19d7c0f1816664980c0a9875d01b27cdf8eedddb639d9e108ad"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7967d08cf06dee78443b874f98c98036f624f3a4e73e11f9f64f5be4d25393cf"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c2b5031f63e331e3839b40aed2dd6f191e9c07edbde303e7876846ea1946995"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fcff63213e8e6e47770541a4607175404f47cbb3ebea7b6058cc82d524a0e424"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a881931aa470808df94a8c380eed2bbbc76cd9dc622310f99665658c821eb6d"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34276fd796040bf0993ab33a369aa572e6979c7aab225a88893667ad8eac8f7a"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eff0ac9dbe711a4aee69bf04a83896aa9b85f19641264053a9f6d48573abb7dd"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1503bccbeb36d5527790c3930327704c39af22de3112f1b1666a9f3ce15ee204"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:52aa6992700996af31f375de0c6bacd402b0097fe40b53c426b9f51a90ebabc7"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e09a3942ecbdee5cce73ea9d42da82b81b72ac1bf031ce069b93b5adf4eac8cd"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c7c9ab723cde841fefb34efbad91e87f00a674b1fe1cd0784fde742bf2c154dc"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddc7dacc8ece3a182e7f15cb862d1fd616b46d076cb1ae9dd232b2c38b655874"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ee43c17b173d46a3212baa6ead3ae258eeabdae48c263a01ccf0218c366dd655"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-win32.whl", hash = "sha256:4f87960d57feabfb618e4e0af6e7371645fa26a277860739d6e5d6e0012c92f0"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c"}, + {file = "charset_normalizer-3.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:80d02b6f04e92601a081dd97b23d3128033098bff5d35d392ddcc0476ea11253"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:dca9ab98072a5a54ebacebdc45f53e645336b320c667410b061be1ca588ae709"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0aa869112ef88429ae17820d99c3dd9504c9e9c671d3c246f3d7442cb051084"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c0afc6800ba57ccc350374c5bd6150419915d95ce93cdbab2d783d75eaf30ecb"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dcd882da75ef9adf94903b1e3b9419e8aa8fb4c7396822b834b9ef7fb96954f"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2e06a3a98f916dd41d27f3105e02e7a40181c98c94b9158733d03a6f80506c09"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bd128f206a7752ae1f2ab6c61bf8a24ba28913a10df8b14c2637b973ff97a80"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c8f3d67aeaf55f017982b73683f0e7342ba2f6635a78f69ce89ebb26aa411e5c"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fe9753dfee015c570d73df76f899f18444d41388bffcde097deba51c4fadbb9f"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:92888bb3187c5ba50500b00b3b310c9f2c651709d28036077680cb5255450a03"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d008d90a7f2471519aef0c90dfbe73b3e6e4d5e66ac48e19154c17e89e98b604"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31f3930700408d211f13378ccbe1c40845d8da54bd0681fac3a9b5aae81c7aa8"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2a925889534b3748302dae5dead07cc13480de1dac3aea80a941b729b471ef93"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f5ec61164adcec446f8969a3358ec3f9b26bbda3b9213e5586d219afa8df2915"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-win32.whl", hash = "sha256:598a11a2c7ebaa5334bf698bf29568c9c390abac6a154d8170fedecd1cea38c5"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:7fdde2c9fd9e3eca40631e024664cf2584272cc8f96308cbe5fdfc930f51d8bc"}, + {file = "charset_normalizer-3.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d1befeed746d247c81127bb14de9dc3d30edb6e5976d34f83f86ed262b1d9105"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:498dc3188ca05a68231ac3fdbfc7f57eb67e1343c30e0fea17f8218c1599b253"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e242bb1c5e76e97dfa9e7f209a71e93a01d7f19ffdd5cfbb2e2d55b4f08f8ab0"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def79fa35ef0cef8d2accec024f4fdc7ead3012ff02f5215c783f39f03ef8cfc"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04851f73ae72b8413dddadb16a49dfee95263553741fd42d546f7d66907e6be5"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:183b88127acdb4fabe59d951ab424faf1af7b63cdbb5f776186c1ea2ffcaed98"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:16fa0eccf81304b79c5cd87f9271c3b85dd9dd99245e4422ae9c0dd45e0f99d3"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:7441d755b7ab94f8d4eb3e43ec05482d760842fd263d003a99102d742cd835e2"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:ca403d7e4798f525fdfc78e258820419cbbd0f0ecbab9de7840e3c017cf6b8cf"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:df29a0a7107f7011e77f4eebdddec4c7331e24d787a0b21a46d63bdf7445da95"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f3c96f633825733f735c5a9cf21d21a257d8e1edf0b1cee0a064b9c424ca0f7d"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-win32.whl", hash = "sha256:281cb91036248400f4cc957495cccd44c275c2e0c5854f7e45ac5cf7dc193847"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8"}, + {file = "charset_normalizer-3.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:915563965d418f986e7e145accc592eae9e1a1be3566ff98a05d7a9ec42a76e1"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65cd72beeeca9d3aaea1201e5923859f308f952f9c71de93f06063c79f0f7a3b"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b7fd005a73d9e657273b7a10dc71a9e03c8fb9ee6999798d6918ce095b81ac7f"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e54da4baf05720032d527874d40b65fa4d7e5c6c6a43d0c3adbeffcaf275a2b3"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:124fbf1a8ff966d87ae05bb8bd45a71f966055ed8bba320d0c7cf450bc5f4d0e"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28b4f0d66fb834ff90f28209ac7bce77868c45d8c93e26f906709d9b7c2e1af9"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58ca3755ee7ff7f59b57789ec9833c9de9ea275405cdd240eda1f193112e398a"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:443eae2bf318abeaf6f15d785138f71fd6de770e99a92158b8b814265e079115"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:58f361dcbab699cf8f42db3f47c8e7fd1036f138c23a5d08de9fde5f425a730c"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1b4cbc7c3491ccb4aa17fcd8165649d01cf39f76de1696da8631b5f71b85401d"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:ba0b1d2620edf869789c3879223f52bf2afc5d31b3cb47cc57b3a12c05e2aa9d"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:5e2b6b57e9733d39f0c9fd3185efa6b8e29652c4cd8fe94180272cf6ed9a78c4"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:51cf45226a9b588d0d2b4880c62d686934b63ab0bd79ca23ab0e9762eb27441b"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-win32.whl", hash = "sha256:5fb29fb8cd1a46c27a1bf9613ad5ec2599310d46b4025d9556404a6b6a292800"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:a192e2c40070d92c3ccf777e3a5c4ff515573cd2bb7ed0c537fdadbbec5bbf21"}, + {file = "charset_normalizer-3.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:749e97e1b32313717a565abbe321bc2190bc8b35f1a67e4cdbc7c56c8d8ffe58"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-win32.whl", hash = "sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-win_amd64.whl", hash = "sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036"}, + {file = "charset_normalizer-3.5.2-cp37-abi3-win_arm64.whl", hash = "sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75a3ceed0724d625d64b86ca20aba182e4df462e04c2414fc941c0f523f06aac"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0891b9d3903c5571c03771ca669a4b0ec5618ca722a5c957d3d29cd4e5062848"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fc14a032f813bf5fe624d991960ea83e9715adc27e4c1830a2361eb1d02ac341"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b2bfab86aa71ae13aa41a6a26aab338e0db2b8bc75434b05aea89e011ff35a4"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9bde855991b7e362c146535e3136a50bfaffc0487d38b33ca7e5edefc6e23849"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55ea99acb17b9325618de155a0cd6a2e8f5d10be008113e1d433bbb58db543b2"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:68eb192d85ab8e5f6ec69c2bc6ac0179fbf04a5ac1569d12fbef74883fe102d0"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d913de495d90407cd859d263bee2e5d1a4ed3eb6573c04e70d9ec619a7cbed7f"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:3ddacd27458c45bdacd6bd6db644bfb730efbf9e830310186e3045c9c5be8fb2"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:588461c2e8384d309bd63e5826019b6977bc66d629b99ac8737bb795d7b2cb5a"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:e80e6c2f55656b4824d72065abb4ddd6a525c74bd78a0aab5d9fc2cf4fb5af50"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:d4a7319f304a774bed22115bc891618e45f85065ab44ea6acd07d274e750519a"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:fd1fbe0f116b6e55da77aca2c6ddcddcfac2186cbf78bdebf40fc156efca389d"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-win32.whl", hash = "sha256:93223adc95033dd47133a46ccfc316a0139176fd79085762e27202ec56018f03"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:15bb4005af6320d259dc7593ca84a38d7fe06a421dbcf7b910ae23979101e787"}, + {file = "charset_normalizer-3.5.2-cp39-cp39-win_arm64.whl", hash = "sha256:2cc961b171b3f3440f410489ab3573e86aea8736134ebbb40ea1338b7f0831bc"}, + {file = "charset_normalizer-3.5.2-py3-none-any.whl", hash = "sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685"}, + {file = "charset_normalizer-3.5.2.tar.gz", hash = "sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef"}, ] [[package]] name = "click" -version = "8.4.1" +version = "8.5.0" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["main", "docs"] files = [ - {file = "click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2"}, - {file = "click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96"}, + {file = "click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360"}, + {file = "click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34"}, ] -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - [[package]] name = "colorama" version = "0.4.6" @@ -558,18 +658,18 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\"", dev = "sys_platform == \"win32\"", docs = "sys_platform == \"win32\"", test = "sys_platform == \"win32\""} +markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\"", dev = "sys_platform == \"win32\"", test = "sys_platform == \"win32\""} [[package]] name = "colorlog" -version = "6.10.1" +version = "6.12.0" description = "Add colours to the output of Python's logging module." optional = false python-versions = ">=3.6" groups = ["main"] files = [ - {file = "colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c"}, - {file = "colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321"}, + {file = "colorlog-6.12.0-py3-none-any.whl", hash = "sha256:30d392604e9110045a2c2aeefc27d7a017abbab63f3a8aee594eac0801df784e"}, + {file = "colorlog-6.12.0.tar.gz", hash = "sha256:2a7924c1dadf18b22a0eb8b06d1c7b01d5341707ec1641eb6fcc4fde0c3e8e5f"}, ] [package.dependencies] @@ -595,103 +695,134 @@ test = ["pytest"] [[package]] name = "coverage" -version = "7.14.3" +version = "7.16.2" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" groups = ["test"] +markers = "python_version == \"3.10\"" files = [ - {file = "coverage-7.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:360bec1f58e7243e3405d3bdf7a1a8115aa9b448d54dc7cd6f7b7e0e9406b62e"}, - {file = "coverage-7.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed68faa5e85de2f3e400bc3f122e5c82735a58c8bb24b9f63a2215954ba17b2d"}, - {file = "coverage-7.14.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:830c1fca669c572dec37ce9c838224ee45aac5be0f6961edf871e82e49d6537c"}, - {file = "coverage-7.14.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a64caee2193563601dbaaa55fe2dcf597debef04a2f8f1fa8a07aa4bb7ac7a1e"}, - {file = "coverage-7.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0096fd7559178f0cc9cf088f2dbd2a02ef85bacaa69732c633517286b4494610"}, - {file = "coverage-7.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6197e5a00183c11a8ce7c6abd18be1a9189fd8399084ffc95196f4f0db4f2137"}, - {file = "coverage-7.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7dfe427045520d6abca33687dfef767b4f635015893a1816c5decb12eb72ce18"}, - {file = "coverage-7.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9a3f142070eb7b82fc4085a55d887396f9c4e21250bccebe2ba22502c45b9647"}, - {file = "coverage-7.14.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64b2055bb6e0dc945af35cdeceb3633e6ed9273475ef3af85592410fd6803803"}, - {file = "coverage-7.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1551b4caac3e3ec9f2bfcec6bf3776e01c0edbdd2e240431a50ca1a1aac72c27"}, - {file = "coverage-7.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:583d50d59142f8549470bd6390471d0fe8b8c8d69d6a0f28ac71e05380cef640"}, - {file = "coverage-7.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0bb8a6bc7015efdf8a928753b25da1b9ca2d6f24ef04d2ee0688e486f32aae7"}, - {file = "coverage-7.14.3-cp310-cp310-win32.whl", hash = "sha256:d48400185564042287dc487c1f016a3397f18ab4f4c5d5ec36edc218f7ffa35b"}, - {file = "coverage-7.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:eadea7aba74e40adee867a8c0eec17b820b061d308a4b014f7a0e118c2b0aa61"}, - {file = "coverage-7.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e574801e1d643561594aa021206c46d80b257e9853087090ba97bed8b0a509d3"}, - {file = "coverage-7.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f82b6bb7d75a2613e85d07cefa3a8c973d0544a8993337f6e2728e4a1e94c305"}, - {file = "coverage-7.14.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a2335ea5fed26af2e831094964fa3f8fae60b45f7e37fcc2d3b615b2add3ad87"}, - {file = "coverage-7.14.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fbb8c3a98e779013786ae01d229662aeacbc77100efbd3f2f245219ace5af700"}, - {file = "coverage-7.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac082660de8f429ba0ea363595abb838998570b9a7546777c60f413ab902bbde"}, - {file = "coverage-7.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac012839ff7e396030f1e94e10553a431d14e4de2ab65cb3acb72bbd5628ca2"}, - {file = "coverage-7.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5952f8c1bda2a5347154450379316e6dfa4d934d62ca35f6784451e6f55074fb"}, - {file = "coverage-7.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8cf0f2509acb4619e2471a1951089054dd58ebea7a912066d2ea56dd4c24ca4a"}, - {file = "coverage-7.14.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2e41fd3aab806770008279a93879b0924b16247e09ab537c043d08bbca53b4ab"}, - {file = "coverage-7.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f0a47095963cfe054e0df178daca95aec21e680d6076da807c3add28dfe920f7"}, - {file = "coverage-7.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a090cbf9521e78ffdb2fcf448b72902afe9f5923ff6a12d5c0d0120200348af9"}, - {file = "coverage-7.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d310baf69a4fbe8a098ce727e4808a34866ac718a6f759ae659cbd3221358bc"}, - {file = "coverage-7.14.3-cp311-cp311-win32.whl", hash = "sha256:74fdd718d88fe144f4579b8747873a07ec3f04cb837d5faec5a25d9e22fa31a8"}, - {file = "coverage-7.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:cc96aa922e21d4bc5d5ed3c915cef27dfcbc13686f47d5e378d647fbfba655a2"}, - {file = "coverage-7.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:c66f9f9d4f1e9712eb9b1de5310f881d4e2188cfcba5065e1a8490f38687f2c4"}, - {file = "coverage-7.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d74ff26299c4879ce3a4d826f9d3d4d556fd285fde7bbce3c0ef5a8ab1cec24"}, - {file = "coverage-7.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:96150a9cf3468ea20f0bc5d0e21b3df8972c31480ef90fa7614b773cc6429665"}, - {file = "coverage-7.14.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:27d07a46500ba23515b838dbcf52512026af04090755cf6cc64166d88c9b9a1a"}, - {file = "coverage-7.14.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:621e13c6108234d7960aaf5762ab5c3c00f33c30c15af06dcbff0c73bf112727"}, - {file = "coverage-7.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b60ca6d8af70473491a15a343cbabab2e8f9ea66a4376e81c7aa24876a6f977"}, - {file = "coverage-7.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c90a7cdd5e380e1ce02f19792e2ac2fbfbf177e35a27e69fd3e873b30d895c0c"}, - {file = "coverage-7.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d788e5fd55347eef06ca0732c77d04a264de67e8ff24631270cdff3767a60cf"}, - {file = "coverage-7.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62c7f79db2851c95ef020e5d28b97afde3daf9f7febcd35b53e05638f729063f"}, - {file = "coverage-7.14.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:90f7608aeb5d9b60b523b9fb2a4ee1973867cc4865a3f26fe6c7577073b70205"}, - {file = "coverage-7.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1e3b91f9c4740aeb571ecf82e5e8d8e4ab62d34fcb5a5d4e5baa38c6f7d2857c"}, - {file = "coverage-7.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c946099774a7699de03cbd0ff0a64e21aed4525eed9d959adde4afe6d15758ef"}, - {file = "coverage-7.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16b206e521feb8b7133a45754643dead0538489cf8b783b90cf5f4e3299625fd"}, - {file = "coverage-7.14.3-cp312-cp312-win32.whl", hash = "sha256:ea3169c7116eb6cdf7608c6c7da9ecfcb3da40688e3a510fac2d1d2bafd6dc35"}, - {file = "coverage-7.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:7ea52fc08f007bcc494d4bb3df3851e95843d881860ba38fe2c64dc100db5e7d"}, - {file = "coverage-7.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:8cec0ad652ec57790970d817490105bd917d783c2f7b38d6b58a0ca312e1a336"}, - {file = "coverage-7.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47968988b367990ae4ab17523790c38cd125e02c6bfd379b6022be2d40bdc38c"}, - {file = "coverage-7.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0ee68f5c34812780f3a7063382c0a9fcbb99985b7ddcdcaa626e4f3fb2e0783a"}, - {file = "coverage-7.14.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fa9e5c6857a7e80fa22ace5cf3550ae392bbfc322f1d8dd2d2d5a8be38cec027"}, - {file = "coverage-7.14.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98a0859b0e98e43e1178a9402e19c8127766b14f7109a374d976e5a62c0e5c73"}, - {file = "coverage-7.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69918344541ed9c8368566c2adc03c0e33d4550d7faa87d1b35e49b6a3286ea9"}, - {file = "coverage-7.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b7f300ac92cd4b570724c8ffbbd0c130fee298d2447f41d5a3abf58976fae1de"}, - {file = "coverage-7.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11a7ec9f97ab950f4c5af62229befc7faf208fdbc0116d3902d7e306cf2c5abd"}, - {file = "coverage-7.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a571bd889cd36c5922ce8e42e059f9d37d02301531d11374afa4c87a578625d5"}, - {file = "coverage-7.14.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de76caefc8deabb0dd1678b6a980be97d14c8d87e213ac194dbf8b09e96d63fb"}, - {file = "coverage-7.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d20a15c622194234161535459affa8f7905830391c9ccfa060d495dbfe3a1c7f"}, - {file = "coverage-7.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b488bd4b23397db62e7a9459129d01ff06a846582a732efd24834b24a6ada498"}, - {file = "coverage-7.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a3693b4153394d265f44fb855fdc80e72403024d4d6f91c4871b334d028e4e0"}, - {file = "coverage-7.14.3-cp313-cp313-win32.whl", hash = "sha256:338b19131ab1a6b767b462bfcbaa692e7ae22f24463e39d49b02a83410ff6b37"}, - {file = "coverage-7.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:b3d77f7f196abdef7e01415de1bce09f216189e83e58159cfeef2b92d0464994"}, - {file = "coverage-7.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:e6230e688c7c3e65cedd41a774eb4ec221adc6bfee13768231015b702d5e4150"}, - {file = "coverage-7.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:605ab2b566a22bd94834529d66d295c364aba84afd3e5498285c7a524017b1fc"}, - {file = "coverage-7.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3c2134809e80fac091bfed18a6991b5a5eb5df5ae32b17ac4f4f99864b73dd7"}, - {file = "coverage-7.14.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c02efd507227bde9969cab0db8f48890eb3b5dcad6afac57a4792df4133543ce"}, - {file = "coverage-7.14.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1bb93c2aa61d2a5b38f1526546d95cf4132cb681e541a337bf8dfd092be816e5"}, - {file = "coverage-7.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f502e948e03e866538048bba081c075caaa62e5bda6ea5b7432e45f587eb462a"}, - {file = "coverage-7.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9973ef2463f8e6cfb61a6324126bb3e17d67a85f22f58d856e583ea2e3ca6501"}, - {file = "coverage-7.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9be4e7d4c5ca0427889f8f9d614bd630c2be741b1de7699bca3b2b6c0e41003e"}, - {file = "coverage-7.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a574912f3bde4b0619f6e97d01aa590b70998859244793769eb3a6df78ee56d3"}, - {file = "coverage-7.14.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e343fb086c9cd780b38622fea7c369acd64c1a0724312149b5d769c387a2b1f5"}, - {file = "coverage-7.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3c68df8e61f1e09633fefc7538297145623957a048534368c9d212782aa5e845"}, - {file = "coverage-7.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3e5b550a128419373c2f6cec28a244207013ef15f5cbcff6a5ca09d1dfaaf027"}, - {file = "coverage-7.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2bfc4dd0a912329eccc7484a7d0b2a38032b38c40663b1e1ac595f10c457954b"}, - {file = "coverage-7.14.3-cp314-cp314-win32.whl", hash = "sha256:0423d64c013057a06e70f070f073cec4b0cbc7d2b27f3c7007292f2ff1d52965"}, - {file = "coverage-7.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:92c22e19ce64ca3f2ad751f16f14df1468b4c231bd6af97185063a9c292a0cb3"}, - {file = "coverage-7.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:41de778bd41780586e2b04912079c73089ab5d839624e28db3bdb26de638da92"}, - {file = "coverage-7.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8427f370ca67db4c975d2a26acfc0e5783ca0b52444dbc50278ace0f35445949"}, - {file = "coverage-7.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8e88f335544a47e22ae2e45b344772925ec65166555c958720d5ed971880891"}, - {file = "coverage-7.14.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:beaab199b9e5ceaf5a225e16a9d4df136f2a1eae0a5c20de1e277c8a5225f388"}, - {file = "coverage-7.14.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3ff255799f5a1676c71c1c32ec01fd043aa09d57b3d95764b24992757184784"}, - {file = "coverage-7.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:878832eaac515b62decfa76965aed558775f86bf1fc8cca76993c0c84ae31aed"}, - {file = "coverage-7.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:611e62cb9386096d81b63e0a05330750268617231e7bd598e1fe77482a2c58a5"}, - {file = "coverage-7.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:02c41de2a88011b893050fc9830267d927a50a215f7ad5ec17349db7090ccf26"}, - {file = "coverage-7.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:526ce9721116af23b1065089f0b75046fe521e7772ab94b641cd66b7a0421889"}, - {file = "coverage-7.14.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e4ed44705ca4bead6fc977a8b741f2145608289b33c8a9b42a95d0f15aedbf4d"}, - {file = "coverage-7.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2415902f385a23dcc4ccd26e0ba803249a169af6a930c003a4c715eeb9a5444e"}, - {file = "coverage-7.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b75ee850fc2d7c831e883220c445b035f2224de2ba6103f1e56dbd237ab913f7"}, - {file = "coverage-7.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dc9b4e35e7c3920e925ba7f14886fd5fbe481232754624e832ddba66c7535635"}, - {file = "coverage-7.14.3-cp314-cp314t-win32.whl", hash = "sha256:7b27c822a8161afbe48e99f1adfb098d270ae7e0f7d7b0555ce110529bdb69cc"}, - {file = "coverage-7.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:39e1dbbb6ff2c338e0196a482558a792a1de3aa64261196f5cdb3da016ad9cda"}, - {file = "coverage-7.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:68520c90babfa2d560eca6d497921ed3a4f469623bd709733124491b2aa8ef3f"}, - {file = "coverage-7.14.3-py3-none-any.whl", hash = "sha256:fb7e18afb6e903c1a92401a2f0501ac277dca527bb9ca6fe1f691a8a0026a0e8"}, - {file = "coverage-7.14.3.tar.gz", hash = "sha256:1a7563a443f3d53fdeb040ec8c9f7466aed7ca3dc5891aa09d3ca3625fa4387f"}, + {file = "coverage-7.16.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:23219888477edd736b6fcaec1272d47d93b926e999641ffea7e53a1738e70b2b"}, + {file = "coverage-7.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40c0f00899fe6181ae7f434ceb200e51f5ee4b8ed10e3b5f0b605f0cae15da87"}, + {file = "coverage-7.16.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4624f80732f6b427ac58f1f59c577a0994a12e8174b5af6a027b4b58795d4c3"}, + {file = "coverage-7.16.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:191803c4996b499fcd78c2ad5e5f767dcc53cb4dc6de6d6a741b443a1821ef02"}, + {file = "coverage-7.16.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fd670ac43b709c575aefc25bf52d8a598a3bc5017bddfd0a179152ab06a2deb"}, + {file = "coverage-7.16.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:705e5af11d34647efdc170c7840b6857c81cf74be96419a553f237e68e62cb72"}, + {file = "coverage-7.16.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8afd9bf35cc6a1f22eb3634808fa8e0b91902459c5721ef2e4461dfe771d7f08"}, + {file = "coverage-7.16.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3f43bac1856ba269b905302778d4df433d6006489a192174ad77ac528e395032"}, + {file = "coverage-7.16.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f8475460aa33ee28ac896ab1156d0bb3b6c639f7f8383c2677d3359eb35f8205"}, + {file = "coverage-7.16.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d6276d78f6fca7d0ac066d5da4165c5acd07829e8305c2cb900b738fb3a75a72"}, + {file = "coverage-7.16.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:736fde09ea39646d11f8e3b76bd3425c075aa4dd45f24891970bb77c14ff20f5"}, + {file = "coverage-7.16.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c85d54e7e8a2ca932fe8399301af9b8d5907ea2a455ffaff6e7d1208db83b943"}, + {file = "coverage-7.16.2-cp310-cp310-win32.whl", hash = "sha256:5139009b5efd2194fc168ee9362f0e191ba612ef5d29242f9269c22f9b8f80c7"}, + {file = "coverage-7.16.2-cp310-cp310-win_amd64.whl", hash = "sha256:c3305c38a2fa21a4254f2ace7dd9ef5fc569c9a558b66e7017650b3d637fb95e"}, + {file = "coverage-7.16.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:732d950e51f3ba4fb6209c73250f3e8924fefca42953ee04a9e65d8c02414d7d"}, + {file = "coverage-7.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5dca0bb66b4c3d624ba047887bf70270030c150692d543cb501293dc38a9f4b5"}, + {file = "coverage-7.16.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:af2a2a8c7c74de0559e0c368d94c8def9e16c58faaee33a0bf081057c4227e3b"}, + {file = "coverage-7.16.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db5f8394e17f877a625b257f2ba0ce8e728a499c2c1579ad66220272cd3df510"}, + {file = "coverage-7.16.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3146d2317c75f70df2509066d979dadd941f7021cdf9b5db4bcd8568258e25"}, + {file = "coverage-7.16.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e1d0ced76318bab499693ff25f64faa343415187cb2e4d7befdfdd391a1cf6a"}, + {file = "coverage-7.16.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:af98ad5ed9d6daaca956201e00bb429a7eb2b080426686f70a20353e0f9839f5"}, + {file = "coverage-7.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d56e4d21c56d2046447733f8b118409597db48c01efe898ee9ac24e858ec2d6"}, + {file = "coverage-7.16.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1d5d0e3b660506fb84f995814e3118a21efdc0c8eb80127da1be627d90093c17"}, + {file = "coverage-7.16.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:17228fbca0f22976f797be94e975dcd237799c657d49551c7de1e0654d1202e9"}, + {file = "coverage-7.16.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:bc0b0ac781d489304b741269857f1f8338b7a26b1b89c06c0344658001ec0035"}, + {file = "coverage-7.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bf1bd822ec4e387ed245bed0d71151582cf7be9e5309bc4145eefe36083d5878"}, + {file = "coverage-7.16.2-cp311-cp311-win32.whl", hash = "sha256:7ed238d227e23cc300c3d464babdaf9f6ddc740aa1b15a77ae96136e6a7c4516"}, + {file = "coverage-7.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:a90700f743e29aa3d75a6ff5f01953176a889c00e526194bc4d281731b88d99d"}, + {file = "coverage-7.16.2-cp311-cp311-win_arm64.whl", hash = "sha256:a336eec40e3520d369b8a6cdabb4f596e69a8b42927ca074aa1452fed943238a"}, + {file = "coverage-7.16.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:218d742afca2b5ad5ca759e93eddedfbcc6eadf8322f080dcefc40b7bd4e2d48"}, + {file = "coverage-7.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9a638be322a8d76a41cdb17781c7f82aaee6a66493d8ffb7e2c09ee22423d99"}, + {file = "coverage-7.16.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:724bd0f1e81856b35e59fc98cf7b4e544a3cb662e4e0864dca73d4326ee9d808"}, + {file = "coverage-7.16.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5375ebd99038021b35e99dc88255022912c06565d316212f4a576e4b08d30f5d"}, + {file = "coverage-7.16.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a076277ca9f5750cc230f0f578ebd2620cec60255b25707361699fef6fb465c"}, + {file = "coverage-7.16.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:58d4a54c6ea672afef66d49be922a2c69826c5ae1a42a9cd94f0c9c2bacdf800"}, + {file = "coverage-7.16.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcbcfcc059117284c603ff8cb61a65872512882f84a8cf0339241f7f7c2f148"}, + {file = "coverage-7.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afdf43b72ef3876c1fe66423b91466e37877c9e81e8cec70542b7e8525b9d1b7"}, + {file = "coverage-7.16.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9acc7f7ec4a1b5f89bd929fde5b8a714f6fafdc6cc18725413d510aa082b47ad"}, + {file = "coverage-7.16.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:80d3f7b48d43ee8fc5e8707a8adb43d743a5a1a85256c25a24f9d6d0e2238fa6"}, + {file = "coverage-7.16.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:126d1af8804d7224421fe991ff65d3ce649081560df7a98b1a5ffff07f9923bd"}, + {file = "coverage-7.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c19cd6d025c1673f22afcd22c7df8a662d779e05d8e3fa6820c22afb895b0206"}, + {file = "coverage-7.16.2-cp312-cp312-win32.whl", hash = "sha256:152877cdc8a07264882cfcd503ba56a3ef6cba56a70e8c70f6eb8ffd7384789a"}, + {file = "coverage-7.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:e6c52d3307824ff93b39efd99e4185d557db40bd841452abfb32e5d9151ca162"}, + {file = "coverage-7.16.2-cp312-cp312-win_arm64.whl", hash = "sha256:a678c0b6b22086ec2427359d22e37445d4a792f5fdbbc744112c7dade65cad02"}, + {file = "coverage-7.16.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1a37c6e478cf687e1aa30a593d19c92c02fad9d122b51ab73f51b8dc7a0c0fc9"}, + {file = "coverage-7.16.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0993d0e90858c03943d3cb152e068a20dd4707924deec84dd2230261baae3b1b"}, + {file = "coverage-7.16.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bb2fc905bbf4e6b7f40806ea79e31515abf6349594cdf0adf27c4215f0463204"}, + {file = "coverage-7.16.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4358b9c8c0125b460407f3017c6cce8156e904b32772c5630d27112f52bdbfe5"}, + {file = "coverage-7.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f15254427c9b33eedac4f198eaf9e356eb4f6214551afb43da6194a2c088ad7"}, + {file = "coverage-7.16.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9a75a4704ff640e46170042eec1f984385a121227c505d5a16ad8e495f452541"}, + {file = "coverage-7.16.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:14253fc7bb15749b849795a06f5d3b6d8bc3fb8a4b5ddc341faf7a89dce205fc"}, + {file = "coverage-7.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:921415102a90637fcc2e3f169f61dad7699ecf690e8639fc21b813acbedc0967"}, + {file = "coverage-7.16.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cce2bc991293f15cc4084ca116827b5900c5f34e1a54dfe83f10ab5c43162eb7"}, + {file = "coverage-7.16.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1fa594c887365b69745f25a416806e61085dd07b94c9eae68a6e20730629b23"}, + {file = "coverage-7.16.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:11e597173af1dc33d5f8a7332ada544199269a223af1ee1770ddd5e245ad0fe8"}, + {file = "coverage-7.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3e7f99698ba3a7d13988bdd984b7ebf13af4dbe2166dc8502eef90d77603b0a4"}, + {file = "coverage-7.16.2-cp313-cp313-win32.whl", hash = "sha256:f80bd9f9633eafc73d0a913ba2645c96ba58bba1befc30590f7c0fbfde59d865"}, + {file = "coverage-7.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:8be099e979fc42559328a21828281b4578304191ae46ed4e80a407048a82eee6"}, + {file = "coverage-7.16.2-cp313-cp313-win_arm64.whl", hash = "sha256:28ff850182a67d117990fa2ce5ea1032836d8c9630dae867e8bdd3bff4533b79"}, + {file = "coverage-7.16.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4ee546b9e4872ffa194bf07ac87bfa1202ebb824d0795dc1ef22f175545ca90a"}, + {file = "coverage-7.16.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a2fac6895eb299a2e52d7bbb8fb3903502b9da8d3f5309ceb16ec40c646b58ee"}, + {file = "coverage-7.16.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:57ff3783f99d75a1e81dd56a9737eb5665e6736a5d93258ba596b6dcad8fd05b"}, + {file = "coverage-7.16.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f37886699cb9abd29958247d718628d5bc6f39e623dff66a09e546c42a7e03"}, + {file = "coverage-7.16.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fd7a86fdda7cb6d616d178654bd0ad6bc0f3f33c2e478aa598500a1a9e34eda"}, + {file = "coverage-7.16.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ac0f3b379c94acc2f7dce5f5f0b24d44fa1cc6a509717ef83dfee07450c2117c"}, + {file = "coverage-7.16.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d0732c83746bc24123c581a85d9dd96b70ddb538c9076020aa1a041790361e9"}, + {file = "coverage-7.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b451c68218c150f616bc9649783ec8de76a59792c759b43aa0c9c0466a465e4"}, + {file = "coverage-7.16.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a56ac4fa5a75c7e182e8f62600cfb4aff43c5ed7356a034f3557659c3bec1d90"}, + {file = "coverage-7.16.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4cc4f73aa3fabc36e32046d6cd2971405948d8a903636508a3d3b2f9128b3a95"}, + {file = "coverage-7.16.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:723dcdab91357159b722935b500ee8abc0a66c8c432e1e9fabf4cc7598952de8"}, + {file = "coverage-7.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5397e21a90dde0e9c6896b77ded8f0be26b66f8b22b33aed41f6043ed95d55e6"}, + {file = "coverage-7.16.2-cp314-cp314-win32.whl", hash = "sha256:848893e1d361448c113dc2f0913503522a6f7be231d0e38333d2a22d9698a011"}, + {file = "coverage-7.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:5a27b731c171e43dc8b5f32b76a5051dde2ec9b9366c87028f08a7088ebc2c7b"}, + {file = "coverage-7.16.2-cp314-cp314-win_arm64.whl", hash = "sha256:1c569a9fd25505f1cd6bea90588818f90373ce90e2632e2cacf19ddbd6e14fdb"}, + {file = "coverage-7.16.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d93db87adb6b1c1b408dce4763314b55d76a9f589e96783a84ac9e7689e48bdf"}, + {file = "coverage-7.16.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aa62c85046473959c13ba9edca9dc90a77d5c1095b1ba313556314d77fe5b036"}, + {file = "coverage-7.16.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:db76506aa5416081f3e8974ae0f7965c58ada0bb0ef7339ac86099588dbb20d3"}, + {file = "coverage-7.16.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a0f2285329dac10ab08f79cb11f5692c497018e6c7c511f95e6fd63a70b8f831"}, + {file = "coverage-7.16.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:382d3346d56b0eec1b793d53a4c88799c8053f516aa3a8d7c44315696954bacf"}, + {file = "coverage-7.16.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:648352b94507179d82637292e7ae8802508d95f78e2f00a705a50b6c48011681"}, + {file = "coverage-7.16.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fb2bde05838fffae1a1bf75e5d411a6cac3e4e9bb97e6640fed8cd47888b33f0"}, + {file = "coverage-7.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6a75180829efb8ae62b4aded25be6ddca1c888d138d2d82e21d93bfbd88f41cb"}, + {file = "coverage-7.16.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:99704f73721e23859112072d522076e11c31744fc96b5652e5dd2018aa4359f7"}, + {file = "coverage-7.16.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:29309ccc86b7f33df7db12813c299f215bbbc470ed6292d0bedd63ffae1ebf64"}, + {file = "coverage-7.16.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:30c1b65d529e46569899fadca59e4a87c1faf2886923f1307ba61e654d4f3c20"}, + {file = "coverage-7.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcf4bc2aab4e16b1c4c0c2005918f23a7dd5d7821ddae82caed9e3342dc2fcce"}, + {file = "coverage-7.16.2-cp314-cp314t-win32.whl", hash = "sha256:a9cd3de0a5bfe7b0e21ee10e1a14e3d61bf52efc88217ab1d95d6ace6970bd46"}, + {file = "coverage-7.16.2-cp314-cp314t-win_amd64.whl", hash = "sha256:611a44e5229a59d7483ce830160e1a0e85f700562c7a5651c7c63fb8f4eb528c"}, + {file = "coverage-7.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:22957cef43ce038641de78ba995de7568d2d6a37c6ddbf7fa0fd7d1ae2344d91"}, + {file = "coverage-7.16.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:414c26dfdb96aac2d570a54e03008f001e32eb2d413705365503648c6bd361d8"}, + {file = "coverage-7.16.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:00d3eb96e9988c45f50cccd1f1496571ac5c1f91386ac02c4d55516eeda19a24"}, + {file = "coverage-7.16.2-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4dbbd1155ca46e6e0b6b89d204428c56ef6a459af21333f365d135a2820e5a09"}, + {file = "coverage-7.16.2-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8fc15cc8d0d06e873c00ef18e1372d605f9aaf3de27d8c24e50782e75bc8b843"}, + {file = "coverage-7.16.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c6afdd69218202bc1758c9a14b86b8cf1084f37ed2ca143e567a103772b16d1"}, + {file = "coverage-7.16.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aba5c63b7afdc749cc9eae943d5b868cba2b261a176378fa1c5a30bc8bc89982"}, + {file = "coverage-7.16.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9174f0af24e5eff248b9dbfe76ec5275a3d19d37edbc2810543f12cf97347a34"}, + {file = "coverage-7.16.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:80e9fdb4c3d926b6ba721d4bf7435bdb869c3527ae7803290361d0ab73db13b6"}, + {file = "coverage-7.16.2-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:7b3bce4a0d05401d70b7d0d5ca783e686bc9d30e81dbd7d980d532609bf809e4"}, + {file = "coverage-7.16.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:44f21e407b278efdfc1ee5e481e00518bd1d500310a30a5fbf2bcbedfef4aaf0"}, + {file = "coverage-7.16.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:59c3926585e1cd1f2190f4b2ac9014de1bbeaf0d5d0587b0dc6b0aa90d17896a"}, + {file = "coverage-7.16.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:066429634299e14dd2d511e1e85f8f9cecc500781f6b41907c0dd6f1baea7e63"}, + {file = "coverage-7.16.2-cp315-cp315-win32.whl", hash = "sha256:893ea9cf86cb8d2546812ac93d973aaf2ee1fb45110a873b014214fd23e3725e"}, + {file = "coverage-7.16.2-cp315-cp315-win_amd64.whl", hash = "sha256:01c6908bc613b420c26c818fe948e1b97dfd041a53c98b01c63bd8321f5c9aae"}, + {file = "coverage-7.16.2-cp315-cp315-win_arm64.whl", hash = "sha256:967d72c835d7a8cf0af99ec813a2d06e3db6df706402f1fe85b31b437645f495"}, + {file = "coverage-7.16.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:98d9c97f51b334b0adce7b964442a9af33c1a00c6ac856984cc5dc8d18f81c75"}, + {file = "coverage-7.16.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:3e861f1071dcc2fec1e88bef0920f6b1eaa66a143555b4f8ab79ba2b0f30ef55"}, + {file = "coverage-7.16.2-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb9d92ecfe2d5b494367c67f7446f8b75b68d8d0c8cf3bc3e6997478be25d9e2"}, + {file = "coverage-7.16.2-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb57acff4a74246ae513c142d4b36e18c389c3aed8661914a53f7cd0071031b2"}, + {file = "coverage-7.16.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:444889f7f66b74e4455c0a97e0e166dd41177f1dca8c0239a47cff25e05ba7e1"}, + {file = "coverage-7.16.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a740ea6f083c6db7b926534d159508f80ba275ab35e722522de0d18d0f56e55f"}, + {file = "coverage-7.16.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8e209591f7c41ae4a9171335cf6156afda0b21de73b02f73f5aa95b2d5fbb08d"}, + {file = "coverage-7.16.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:396bb16e04ce04efbb3df91456ae4e3da918e69ecdf67fb711b0a0fdf35ccce0"}, + {file = "coverage-7.16.2-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:9cdf19874e0d247f32f03609200370343c3c7aa260b191d8c2bb251d36198283"}, + {file = "coverage-7.16.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:fd3d72233eb8b48acc94fa57d44e2d32ce8e7abed02882ccb6d855ccc4ed33ec"}, + {file = "coverage-7.16.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:bb4ffe96aa663cee727659db5a2afeb38c95f8677b747d447b90d6d4874ea2c5"}, + {file = "coverage-7.16.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dba2edfb054f6d4a08df9d1637c39a5aa3865bca6617c13c86be21e45658a59c"}, + {file = "coverage-7.16.2-cp315-cp315t-win32.whl", hash = "sha256:251aed777c47c77aba047096d4542889db089227655711dfc2b9c54ef0e15e35"}, + {file = "coverage-7.16.2-cp315-cp315t-win_amd64.whl", hash = "sha256:2aca0bdfa9e91621d5b09d815357bf63def4fc0e9cb66da67bf2cf93f3b1a6f5"}, + {file = "coverage-7.16.2-cp315-cp315t-win_arm64.whl", hash = "sha256:b88841e654f09732804809e435b3e005a929ffd9998b872b7b213957b8759cb8"}, + {file = "coverage-7.16.2-py3-none-any.whl", hash = "sha256:11d28e9123a9156cb405d8d27b44256c9a58fb5decc2073a8f17862057e3aa0f"}, + {file = "coverage-7.16.2.tar.gz", hash = "sha256:ca64d9f1f384f151b9511bec01126072acd2f313439f8ed015a22d8790aab6fa"}, ] [package.dependencies] @@ -700,44 +831,174 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] +[[package]] +name = "coverage" +version = "7.17.0" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.11" +groups = ["test"] +markers = "python_version >= \"3.11\"" +files = [ + {file = "coverage-7.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:99a7b390cbccc6bf029ca4da3f0f607c218bb4085e9cd6b58a90d14ca12993c3"}, + {file = "coverage-7.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f72640e02a470b57b15cd655820133f6892760209283a363a60e2df932d5f0fe"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2dc9233f39149b5a4f4aba17a4b6bc5cca22e52cfc4b06fdb07d014439b8b2ea"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e8113d3852e9377421a61de1917df27b3abb25404a4ecf69f9e64612c4345b5"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86a39539d10d666bb819c16c94d7edfc1aa2e0531b937f7c549b83d15177c6e"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:559f748446ee799803686b5784e3deb54c70d655ac386a860e06b11672504ba7"}, + {file = "coverage-7.17.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34d65c9e8e257f48a654908005046fbf1bc48d9540ae3776ad08c0acd010e10c"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b7fa0997b8eba7378ac66f31ec7f70164490353ab79b9667d37c0d365e50177"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6ae0453a3390e67f7bfbe34417bbdf5b4ebfc21db23fb446a0a41115eeeca6ca"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:96151262832f3c230ebae23b2d6911ad549e1738465946c97004913ff06112ba"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:cd7397de6cc782f00fe5ac1d75e9b306bcfa49c8b6b8dd6ed1f7709393ad40b3"}, + {file = "coverage-7.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9d1fdf82c6fea57b5e9dada509f9ce9638c8334664e2c98c7f6f1ccf3d143ad2"}, + {file = "coverage-7.17.0-cp311-cp311-win32.whl", hash = "sha256:38c86e5d74bbfd88a393fa3f91257c1225c63aa6db6d5918de3e49ab04f91c6e"}, + {file = "coverage-7.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:2ed3097c6817bc1a7fc8ae0a6131db74aa1150d863cccc308237ff362d3226b2"}, + {file = "coverage-7.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:6052e878b628abcd38fa5f2b64abebc686eed4e2267da42434017a9c6d24f241"}, + {file = "coverage-7.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0cfdc938ca3319d873d1f1252e60620a39aee4ebf9dbd7cc58238109ebda9c51"}, + {file = "coverage-7.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d523da3c2e4bca49417768bcdb1a443c5b359dc44b8b93a3102e118fce912c40"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:adbf3450c0f3f2b2854e282a8758f2b3eddf787eeff24ec9c9bc2cbc38328cc1"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:85932227e9dd38e62cd29c17fefdfecfb919eead6af1e3259522d03cf85de983"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30f5477aac9c9e4a0a641c55f8f8fddd7f3bd91176c912c50f40ffced96b4034"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0b8bb651a4406fb6a983a905309fec1783443c65e1771b41e92740a0f4a1176b"}, + {file = "coverage-7.17.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0edeb6594f080c8de1263cccb239cb316ef77aeb959e2e0f38f2953fcc72a49e"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da94ec0484e84fce5c7bf8d0f54b97767a90d992653a129b35634699d516e0a5"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a25cc5e8c23a590815c6a7aa43624234d25cb23b21b484adb8a0daed59860db5"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db645c81cf4d6bcb392ea1894aad65d55d1588a8472c834b25ca575da3029a56"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6a80a11b90c4a03df1502304cf24ff953255b6b5d076ef79066ecf88baaff31f"}, + {file = "coverage-7.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e4eaf11d6b0f2147f46cfbf06fe7edcca6f095d0120da00ddc2458995ee869fc"}, + {file = "coverage-7.17.0-cp312-cp312-win32.whl", hash = "sha256:912a29f338798541d795e2e25903c6fe7a99dee70a40fe4706a9a124649b523e"}, + {file = "coverage-7.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:88a1a9fb0075b83389dd9b403bb3fce976031b3c2aa0e05e3e57143cf639de69"}, + {file = "coverage-7.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:2c680e838ff9e68d491158625ca2c1ddb17d78d791bba96a31f8b839b4990438"}, + {file = "coverage-7.17.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:84538c040f4e6214f663459d89372ce4e6b2bbdd9d397ef0dca7b3ecd56b8eb2"}, + {file = "coverage-7.17.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:6abfb6a4cf1cb1b67bc8fccdde177e2d2b781d0d34449e7078156547025140e5"}, + {file = "coverage-7.17.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ef65e96f0fc1a7c8b7644c7011d47a8228555e7888ac4d6246f715a7f3d7c398"}, + {file = "coverage-7.17.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:55939ea4c097fda2edabf80fac5e49bef15597b950a9f29fc56292a895245feb"}, + {file = "coverage-7.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bfb6f868af552c0ceb2d19220b8cbcc2be096ca26163ac30e682a13041433429"}, + {file = "coverage-7.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8d823d66afd59566030539c49c7a6acaf3083996682be2dda760c63555842094"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50678474e714c684b82c108e63fbb8f1bcf1266d8293a49980f1d08526f093b5"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:045c1eeb19fb4eae949850a761102fffb3a5f638b97f977912f95843e54f6669"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfb12891a773cbc2132349c77c690a045a882fe3f98b4bda50dfabb01893f9bd"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3067608b4429629f640c1b23d2f69b05d2fd45e00ebb6ee9e8fad220a6c11722"}, + {file = "coverage-7.17.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1762e85cbbe3b5ff9c6dcbcc1b89d5c72320094292b7b9572bf882c85e428b7"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62bb77987d3f72a0aaa77c567ee7f9806bc2e748c8f46fafbe1fdb90dd148398"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6cc31f27240d4e080437e3b8229d92f1d809c9a4f233607385373b73974e183e"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:61cde5222f1933d476647c5a25ed1a8678b7703098cb5a15f4a0aa39967d8c28"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:11f5f7fb2d97a0fe865f5e7bad4eb0619b957637f676bc088d13844adf26ba95"}, + {file = "coverage-7.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e09612b1b82c9df1d2b6c85dee88d5aad3e73d77a8ec0b16ac6a7322cbdf5d9"}, + {file = "coverage-7.17.0-cp313-cp313-win32.whl", hash = "sha256:bd3e9eb493631ff1ca34bf5efac25b5c66724f837bf6182c4fc771c55946423d"}, + {file = "coverage-7.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:f1fc9a7cea899c51d8a53e66c2f082a3d9e461ece32418d3ef0a0b90c32bfd3d"}, + {file = "coverage-7.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:e65a1e61a68493fbfbe44d8bec71b3231d81b9917f62d3dafe0e105072147e83"}, + {file = "coverage-7.17.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:dc5936491e306f2f6f095bd5fba593de11eb0375cee787ca1f245c3a333a523a"}, + {file = "coverage-7.17.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:968c5fb6e0c83cab752d79a70209648ce4d90cae815c60fa229b59e264601062"}, + {file = "coverage-7.17.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:12cb8a20ad904a5811e6660d829a586b216e5254b9b34b76dde8a4d65d827dc6"}, + {file = "coverage-7.17.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:bb5ca66937c55bc85d1a5e9c5900a3e4693a8edf2280a9b3672b7b2661a84b95"}, + {file = "coverage-7.17.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5cc15faaf318ee5040d835a62c75bc6f122d837274f3b5efd47bf9fbd4ac5b85"}, + {file = "coverage-7.17.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7d6d9d396c8b7bcaf76d774a10140b218111484e54c73a5f2a1b75655381748e"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:42e6f170d1ac04b7eb3363310a6c41cdfd82c1fe6bf9ffbc4a86f380bdf091c7"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:acdb5f3c96046efff9dc6c572a6acb2b8d0da09372d7b9a9ed7249cf7d17434f"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3393209f6894713161ee82a9b0e673e58237e0f9733c6638e7a6c0c620ef0b33"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9494d9e99389d39e6a96227749af898d8924ff591cb63944f8a98e68582bf0e"}, + {file = "coverage-7.17.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:032d4c2db4c677eb71d43f2f8b80bf64e9346f7a3f7b6ebb13f1782f3cc6c274"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4343a9378b964eb3cb80fa90406385de91c7e982a769ca9314d848f93bc0d3f2"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:b609df3d6d900464207532c5170a65c6ede8d2e0b91f151f90008a866395cd76"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a01ef92caeedfa4f2ad171a3a8e6fece4565de871d8bca428151bf9929f41048"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a3914e2c98c34b94c26d2add3d2a61ccfd3534fc4eaa879ee290bdbb5184ba6c"}, + {file = "coverage-7.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8a72b26dc5c9a9422e45472b1d67ba5caf273c524c8668cc04b23062663755e"}, + {file = "coverage-7.17.0-cp314-cp314-win32.whl", hash = "sha256:03baf0688e14a5a44ffd1eefa8928dfc92b14565e521174e072b31b971aa1657"}, + {file = "coverage-7.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:3a4459b0f389dd1e51aac3dbfc4ca35b30cae7dc99caa3c0a086d1997a985709"}, + {file = "coverage-7.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32ee5831b15ba239bef5d272ae586ca8acea4a661b5f2941f5a0d380e518b23"}, + {file = "coverage-7.17.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9c083e253e96fdeeaf48133fe836cac86ace2db215d235700e7f7532420a200c"}, + {file = "coverage-7.17.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b88fb4d4d7608cbcebdc7ff7dbffc2115e3b81d6c33a79147cd79d54fcb080f4"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2bf3ee38a40c0570c316b9e7507c472469586906a20f289705898528c31aaea8"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7c570293f001ea91598b90e90cb9b98fddee833e90bbede432cbd218331ccd8e"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:791f9ee09814dd30d9562b4f4eb037a371485ec1c830b53999d2320d20c4e764"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7c5672ecf1b497de7a0b37743bef40e3b58e3207e9fe2ed6682cb0420836a88"}, + {file = "coverage-7.17.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:337f87e62e1c5f43c8198d879f677c342d152f84d3f157add12558853cbb8eaf"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c8a58baa8eb6154c6645eadcc19b6c089deab78c24a612a83206f2a8a5992a7c"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fb5745048ee20f796b0e3ab61831049e749acff188563e2af32f58ede957ac1b"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:7b5abcae74750302ac4bc77351eaee87c450e4abd3b6225eacde3858e260f8b2"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4e153f0bd6509dbf837fd585f489b3328bfd98f2a718a0b15ae63032148b285c"}, + {file = "coverage-7.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e42a835271922fe7bf98c7a05c78d4032f4e52bf5b07fafa1eadfdfc2f0ad22f"}, + {file = "coverage-7.17.0-cp314-cp314t-win32.whl", hash = "sha256:5900055351ec89fdffb34b27898815106529ded29a9496464c1ebd6313fd40fa"}, + {file = "coverage-7.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5ee9c8bd13a59e301dc2aefe7aa8384633406808de051b1982775667094eb673"}, + {file = "coverage-7.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a9cb9a0fb58e6fff5442969af36094ecd6976f4907f179e33b27e15355d8f74"}, + {file = "coverage-7.17.0-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:adabc1079ba844b096799ba8638d8df434aca1613f95bb7d5791af320cf05559"}, + {file = "coverage-7.17.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:6b4593d2871c4c801656dd5861e759f8f5156febd6b6061b02f6a72d64618b7b"}, + {file = "coverage-7.17.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c2c48a36bb970b7265b196032bfd4237c0b373f2b25ef7012d7e905b5b35c19c"}, + {file = "coverage-7.17.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f8bc2cd4c6aa58be1ca243fb8cddb6c3c42031fe4403991ad9cf2b3baed03d9f"}, + {file = "coverage-7.17.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:cbf0b83ce927de6053279ba61342d9f8892afde1385fcba9144cd05d4023d109"}, + {file = "coverage-7.17.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:66efc0fb99cee34626fc17d3adac9c3cd816bfdad563bb1e983c3ef432f566d4"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:58a304efdf2253fb2ea03d45d0a04cc174a92a0cd5279c28c5bb893ce7410968"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aed06129a9f530a948928260d7d2d81c47261355b0127e3c782251b38958463c"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6cdb47a9be2d3313829bed1d296086287682a3ee901ff987b3432b45705b771"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:804fbaf81d09b34e2f8b1159d01e211a50713d743d32fd9b2ca6cf0ae507a66f"}, + {file = "coverage-7.17.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b2afebb96da9a1bbca142f8cc12320c14e5c215e0a49cded4f191f5a31506a28"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:bd04fd23206be9ad3de13b0b39313094fafa66a1f6baf70f5471c004a9fac47d"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:5ebfbc87836529a31d25c2dec1262c38eea852ec1c46c0f690a515a7339d9c5b"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:a59c3b470ac3b2432af3598a996affbeb10236cd899a005c1c4e517fcff8b592"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:7fef12cec4b96f59e09761cbd4fc9bdf4788062d1ae435b920241fc16c8a9549"}, + {file = "coverage-7.17.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:a14e8d69464da9cbe1dc29e7116dbd37296d1d51a695327c478e250349085089"}, + {file = "coverage-7.17.0-cp315-cp315-win32.whl", hash = "sha256:9a66074231799d8884d267234d49f60c90b43a945dfe069646b72f3058cd4e67"}, + {file = "coverage-7.17.0-cp315-cp315-win_amd64.whl", hash = "sha256:79cf71e5f9a48877ccfed5baa2c72f45af4feeaa596249e23eb15b473134830e"}, + {file = "coverage-7.17.0-cp315-cp315-win_arm64.whl", hash = "sha256:09e563b49c81cd0a33734e926d7a4a6ef1778d74304cde2dcc3aff8b30ea1036"}, + {file = "coverage-7.17.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:a5e2e0625dfc789af0b2e10acfc106b352f314fffbd19e6392a7961c8cbe5d9a"}, + {file = "coverage-7.17.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:194562ab1eff0d85aee8a17ce4c5c0bc1ebf70a629a7145257123156ad100fd6"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:85682ca89bc4601125dcdff23fa70e6274e13c9ad502fba845e6c274ab2c6569"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0d0fac604a962ff546beb038cba9a5a09d88e6e3f01612fb46f7bd4fe5dc8dca"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72edd4f9483ae30cd1ba4f230687840ef7b58a16afb1c9e81131fd6d64655f36"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91e487ad07c511da77b574820141a7c2da09bc2a4b4090b5be2424295d100697"}, + {file = "coverage-7.17.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1cabb31531002fef5197de5df3c84730aef01e750e25154b9813a2583435aba2"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c9ac886242bbd50fb35a2242ba79555a99891e657da33f98b7ca968e3488a350"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d45cdc495671fc99268ff3653e0da4244972e9fc0e1fc3ea9de18781ad28963a"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:12961f738d717a046d4bf3f8d1727eb3ebe2ed5478ff96cd6233d4dfc39323d3"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1ee0a982b0e1230b3edf768a7dd022bd2377303763895327ac62deb5efacee03"}, + {file = "coverage-7.17.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:460679f53893081edb94eeed011d26be37228ba649b853fd2aed7e4394fb1f35"}, + {file = "coverage-7.17.0-cp315-cp315t-win32.whl", hash = "sha256:600e121e412cd6bdb52ec105b95c6a04da0c9710d1534f6e64a42db667907802"}, + {file = "coverage-7.17.0-cp315-cp315t-win_amd64.whl", hash = "sha256:c3e11b81faafb1f063248735c3e2c5c4183fc1db2c7f46782170dd7760dffcbc"}, + {file = "coverage-7.17.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d472ee1c64868a917a47f02ffb4ac8c22b477f7589d57a3eb0c58b1ad4f0137c"}, + {file = "coverage-7.17.0-py3-none-any.whl", hash = "sha256:bac487dce0d2aff28ddf1a533a916f771416eee0e493960f611e49037b878e9d"}, + {file = "coverage-7.17.0.tar.gz", hash = "sha256:cc010a0a7fbdd9bb8bdfd0d3f04a5151275860213cbd7e2b876b29778489c712"}, +] + [[package]] name = "debugpy" -version = "1.8.21" +version = "1.8.22" description = "An implementation of the Debug Adapter Protocol for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "debugpy-1.8.21-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:8eeab7b5462f683452c57c0126aaa5ec4e974ddb705f39ba87dff8818c8e08f9"}, - {file = "debugpy-1.8.21-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:0fddfdc130ac6d8bfc0415b0409822fa901c8f310e5c945ac5653a0352532344"}, - {file = "debugpy-1.8.21-cp310-cp310-win32.whl", hash = "sha256:72b5d676c4cbfac3bac5bb01c138a4656e843f93f03ce2a5f4e394ad49fbee73"}, - {file = "debugpy-1.8.21-cp310-cp310-win_amd64.whl", hash = "sha256:a7fe47fd23da57b9e0bec3f4a8ee65a2dc55782455ed7f2141d75ab5d2eaeef5"}, - {file = "debugpy-1.8.21-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:da456226c7b4c69e35dbe35dcee6623d912000a77816db7856a41af1c72a0264"}, - {file = "debugpy-1.8.21-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:f68b891688e61bdc08b8d364d919ff0051e0b94657b39dcd027bc3173edb7cdc"}, - {file = "debugpy-1.8.21-cp311-cp311-win32.whl", hash = "sha256:f843a8b08c2edeaf9b1582eed4f25441af21a297c22ff16bf76a662557aa9c9e"}, - {file = "debugpy-1.8.21-cp311-cp311-win_amd64.whl", hash = "sha256:84c564d8cc701d41843b29a92814c1f1bef6798724ca9d675c284ad9f6a547d7"}, - {file = "debugpy-1.8.21-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:9f96713896f39c3dff0ee841f47320c3f2983d33c341e009361bb0ebc79adc4e"}, - {file = "debugpy-1.8.21-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:c193d474f0a211191f2b4449d2d06157c689013035bd952f3b617e0ef422b176"}, - {file = "debugpy-1.8.21-cp312-cp312-win32.whl", hash = "sha256:4743373c1cac7f9e74a1b9915bf1dbe0e900eca657ffb170ae07ac8363205ae9"}, - {file = "debugpy-1.8.21-cp312-cp312-win_amd64.whl", hash = "sha256:bd7ba9dd3daa7c2f942c6ca8d4695a16bf9ac16b63615261c7982bc74f7ed20c"}, - {file = "debugpy-1.8.21-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:13678151fc401e2d68c9880b91e28714f797d40422994572b24560ef80910a88"}, - {file = "debugpy-1.8.21-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:ecbd158386c31ffe71d46f72d44d56e66331ab9b16cad649156d514368f23ab2"}, - {file = "debugpy-1.8.21-cp313-cp313-win32.whl", hash = "sha256:2c2ae706dec41d99a9ca1f7ebc987a83e65578363be6f6b3ac9067504917fae1"}, - {file = "debugpy-1.8.21-cp313-cp313-win_amd64.whl", hash = "sha256:aa648733047443eb1d07682c4ef287d36a54507b643ffdf38b09a3ef002c72a0"}, - {file = "debugpy-1.8.21-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9bb2a685287a2ac9b181cde89edcec64845cb51de7faaa75badb9a698bc24782"}, - {file = "debugpy-1.8.21-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:3d6922439bf33fd38a3e2c447869ebc7b97da5cd3d329ff1ef9bc06c4903437e"}, - {file = "debugpy-1.8.21-cp314-cp314-win32.whl", hash = "sha256:15d4963bd5ffa48f0da0947fd06757fa7621945048a14ad7705431566d3c0e7c"}, - {file = "debugpy-1.8.21-cp314-cp314-win_amd64.whl", hash = "sha256:fe0744a12353406de0ae8ccff0d0a4a666f00801a3db8fd04e7a5f761cd520e8"}, - {file = "debugpy-1.8.21-cp38-cp38-macosx_15_0_x86_64.whl", hash = "sha256:0042da0ecd0a8b50dc4a54395ecd870d258d73fa18776f50c91fdcabdcad2675"}, - {file = "debugpy-1.8.21-cp38-cp38-manylinux_2_34_x86_64.whl", hash = "sha256:ffd932c6796afadab6993ec96745918a8cb2444dbd392074f769db5ea40ab440"}, - {file = "debugpy-1.8.21-cp38-cp38-win32.whl", hash = "sha256:4e7c2d784d78ad4b71a5f8cd7b59c167719ec8a7a0211dbb3eb1bfeda78bc4e2"}, - {file = "debugpy-1.8.21-cp38-cp38-win_amd64.whl", hash = "sha256:aa9d941d6dfe3d0407e4b3ca0b9ec466030e260fbf1174094f68785680f66db6"}, - {file = "debugpy-1.8.21-cp39-cp39-macosx_15_0_x86_64.whl", hash = "sha256:9f5171176a0084b95d2ebe55a4d1f7b2a75b74c5dbec577ebd3a85c740551c36"}, - {file = "debugpy-1.8.21-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:f15c10084f9861b5e8414a48f18f8e4aadf51a98a59e72c16aa28281ca994672"}, - {file = "debugpy-1.8.21-cp39-cp39-win32.whl", hash = "sha256:4e70cc8b5079f885cb43910924ee0aab73b8b6b2a14eff23afdd9895d86e79eb"}, - {file = "debugpy-1.8.21-cp39-cp39-win_amd64.whl", hash = "sha256:e935f9dc0501be523c8a8e1853c39432e1354e9ece717ae5998fd2371c4542c3"}, - {file = "debugpy-1.8.21-py2.py3-none-any.whl", hash = "sha256:b1e37d333663c8851516a47364ef473da127f9caebe4417e6df6f5825a7e9a92"}, - {file = "debugpy-1.8.21.tar.gz", hash = "sha256:a3c53278e84c94e11bd87c53970ec391d1a67396c8b22609fcac576520e611a6"}, + {file = "debugpy-1.8.22-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:fa47099176d1d612bef69e9f74658a5687a9f05bd80fb5710f99959bace85735"}, + {file = "debugpy-1.8.22-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:7bf29e0d8ce80b100d37fb333e1b193b790d962739f3067796c2b03ffdc9afee"}, + {file = "debugpy-1.8.22-cp310-cp310-win32.whl", hash = "sha256:56b877b37ed73f0bf53ba7afc394816ff1eb5d701bae24a24ed9e1ea6f7ce34f"}, + {file = "debugpy-1.8.22-cp310-cp310-win_amd64.whl", hash = "sha256:1bd0c6df3c68c0a3f71db8baa3780a953abb65537ec4b3bc6b935ad5b3b3d45c"}, + {file = "debugpy-1.8.22-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:66e4ac3d6e7026e83e7d93d7ee2f51dd4a4e8dff673578d424e60796893e5b2c"}, + {file = "debugpy-1.8.22-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:192b73e8d53bbd60225220c0943627bf249ac93d0ea3090e23c45f3e0ceb6a35"}, + {file = "debugpy-1.8.22-cp311-cp311-win32.whl", hash = "sha256:745e1800ec2961e5660c1a317c0a20e28c0fef4de36c04f17a21c33f2b37a92a"}, + {file = "debugpy-1.8.22-cp311-cp311-win_amd64.whl", hash = "sha256:1e76339d5510bc17e9181dba9577508afcb21aad5728f1a55ef74d7d97d255f3"}, + {file = "debugpy-1.8.22-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ad076f4f66cb8acb79e4384d48b1ea60a0b1957aa0b1112d4887ea3a4df60e0"}, + {file = "debugpy-1.8.22-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:8a697acec45dbc70d17fb5d9f4f61989fc294d273c69de487fd10cb35fdd75eb"}, + {file = "debugpy-1.8.22-cp312-cp312-win32.whl", hash = "sha256:12bc7f368182b517cf26c76a2393fff65354e365fa1552b6241e66edff17997b"}, + {file = "debugpy-1.8.22-cp312-cp312-win_amd64.whl", hash = "sha256:371a4ba4a5975eb958393903f3254cf983da7b1c7c178b3f987ee427f42515e3"}, + {file = "debugpy-1.8.22-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:c21dd7e1ec22556bb41dc3bf6b86c603e440ec889c4acb27d7206354b2106c54"}, + {file = "debugpy-1.8.22-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:c1ffb9953708b648ecf6acd2e5ad2c002bf68785ed618197dd4463a2a7226f39"}, + {file = "debugpy-1.8.22-cp313-cp313-win32.whl", hash = "sha256:ba810a66b437e3c43ca0ce0892404f010338286263ca8e5108442d76a9485337"}, + {file = "debugpy-1.8.22-cp313-cp313-win_amd64.whl", hash = "sha256:f49b1d6cecf326b63c06d7f517e4a6642777f758c58cb799e7bc5a6dd74721c2"}, + {file = "debugpy-1.8.22-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:e6744ac1850c73c2ba7b29a126cf9ab74efd1831190720e8fee17ef5389c8f5d"}, + {file = "debugpy-1.8.22-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:0c1104233340196e5cbf5514e7dfdbb98968e730cfd7fdd6f3d72a082be838a9"}, + {file = "debugpy-1.8.22-cp314-cp314-win32.whl", hash = "sha256:feea785c7bbeb8cfd5b01a63f9061c899dc468c8be81671141a29305774fa294"}, + {file = "debugpy-1.8.22-cp314-cp314-win_amd64.whl", hash = "sha256:d593a330297332ec435f3965c448b6d500e03f33675cf2063178bc76377a788e"}, + {file = "debugpy-1.8.22-cp315-cp315-macosx_15_0_universal2.whl", hash = "sha256:a9eca6ab09a61534e923064400081e016f7e1d8430cc6e8b7a9cecd2f59e2b07"}, + {file = "debugpy-1.8.22-cp315-cp315-manylinux_2_34_x86_64.whl", hash = "sha256:b7dbde1fb822d100802d505b2aed6d0813b1a0a2015d495cb8798b2215d5d1e5"}, + {file = "debugpy-1.8.22-cp315-cp315-win32.whl", hash = "sha256:225d063f81708c2546999e7edfac0198b2d5c2f144797dc64858f867948633e0"}, + {file = "debugpy-1.8.22-cp315-cp315-win_amd64.whl", hash = "sha256:b17a4896520f1c6da09ce76ec8215df0b85b0b6f3617526a4c65c2315340875a"}, + {file = "debugpy-1.8.22-cp39-cp39-macosx_15_0_x86_64.whl", hash = "sha256:cba7b99573ee41f9510c6deeff1cdd0c333748b9dd63d5016413dd57ba9a4593"}, + {file = "debugpy-1.8.22-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:3b7c328cb47b4e2f2b40801936daf8150f88dcb4ffef5080ff119270f0d23626"}, + {file = "debugpy-1.8.22-cp39-cp39-win32.whl", hash = "sha256:ffeeaf4dfe1534375902040df32a4c48f721fd35a4d4d337ac72a44d4176075b"}, + {file = "debugpy-1.8.22-cp39-cp39-win_amd64.whl", hash = "sha256:92fc425308a08f601f3c69afb4719767f9b898276f44b97e6942a713c4c32740"}, + {file = "debugpy-1.8.22-py2.py3-none-any.whl", hash = "sha256:a9e9d3550e15ca479c59333e90845029190531f0cacfedab3b815a57bd913947"}, + {file = "debugpy-1.8.22.tar.gz", hash = "sha256:e489c7268e1c7b41e13b438d9c533d2a7af73fb59bf8cd30fead8286c1c39c4e"}, ] [[package]] @@ -747,6 +1008,7 @@ description = "Decorators for Humans" optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "python_version == \"3.10\"" files = [ {file = "decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c"}, {file = "decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82"}, @@ -798,14 +1060,14 @@ profile = ["gprof2dot (>=2022.7.29)"] [[package]] name = "distlib" -version = "0.4.3" +version = "0.4.4" description = "Distribution utilities" optional = false python-versions = "*" groups = ["dev"] files = [ - {file = "distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b"}, - {file = "distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed"}, + {file = "distlib-0.4.4-py2.py3-none-any.whl", hash = "sha256:cf09d55b1e85a89576b036aeb71550bfd68320f3f21761ca329eadadad918638"}, + {file = "distlib-0.4.4.tar.gz", hash = "sha256:ca4175b4ac7f71223cc30ed7248c45d2b38c2439d8ee6e498ab67ed63cf3508d"}, ] [[package]] @@ -822,14 +1084,14 @@ files = [ [[package]] name = "domdf-python-tools" -version = "3.10.0" +version = "3.11.0" description = "Helpful functions for Pythonβ€‚πŸβ€‚πŸ› οΈ" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" groups = ["docs"] files = [ - {file = "domdf_python_tools-3.10.0-py3-none-any.whl", hash = "sha256:5e71c1be71bbcc1f881d690c8984b60e64298ec256903b3147f068bc33090c36"}, - {file = "domdf_python_tools-3.10.0.tar.gz", hash = "sha256:2ae308d2f4f1e9145f5f4ba57f840fbfd1c2983ee26e4824347789649d3ae298"}, + {file = "domdf_python_tools-3.11.0-py3-none-any.whl", hash = "sha256:561ac19b452cdfee01121f50117a338d03c51b36084d2f9617651113eec6aac8"}, + {file = "domdf_python_tools-3.11.0.tar.gz", hash = "sha256:518ac47b27a799a72b01377f21588e41550a59918c25a74fb55fc7d15fbb2434"}, ] [package.dependencies] @@ -866,7 +1128,7 @@ version = "1.3.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" -groups = ["main", "dev", "test"] +groups = ["main", "dev", "docs", "test"] markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, @@ -896,29 +1158,29 @@ testing = ["hatch", "pre-commit", "pytest", "tox"] [[package]] name = "executing" -version = "2.2.1" +version = "2.3.0" description = "Get the currently executing AST node of a frame, and other information" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017"}, - {file = "executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4"}, + {file = "executing-2.3.0-py3-none-any.whl", hash = "sha256:736e859c9f8701f11fcf516856f26f562e04776387824b43a35a1dfe21c84122"}, + {file = "executing-2.3.0.tar.gz", hash = "sha256:15919cb5d667e5cb4e099511971d00d659573fff2dd5c4e6cd8b71636c7858d2"}, ] [package.extras] -tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich ; python_version >= \"3.11\""] +tests = ["asttokens (>=2.1.0)", "coverage-enable-subprocess", "coverage[toml]", "ipython", "littleutils", "pysource-minimize (>=0.10.1)", "pytest", "rich ; python_version >= \"3.11\""] [[package]] name = "fastjsonschema" -version = "2.21.2" +version = "2.22.2" description = "Fastest Python implementation of JSON schema" optional = false -python-versions = "*" +python-versions = ">=3.10" groups = ["docs"] files = [ - {file = "fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463"}, - {file = "fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de"}, + {file = "fastjsonschema-2.22.2-py3-none-any.whl", hash = "sha256:0fb3915616adac85ccfdd737d26be1089845d2019819505b42d39888458f74d4"}, + {file = "fastjsonschema-2.22.2.tar.gz", hash = "sha256:72064e12356a7d6ef02165be2946b9abadbdf238536e07eb587e3dbaa33099cf"}, ] [package.extras] @@ -926,14 +1188,40 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc [[package]] name = "filelock" -version = "3.29.4" +version = "4.1.0" description = "A platform independent file lock." optional = false python-versions = ">=3.10" groups = ["dev", "docs"] +markers = "python_version == \"3.10\"" +files = [ + {file = "filelock-4.1.0-py3-none-any.whl", hash = "sha256:2ce9818e3e2d8f284c1a964414447ef148d42a5fd5e2a477a7118e574b293ec1"}, + {file = "filelock-4.1.0.tar.gz", hash = "sha256:ad7f724afef953e731b1cc39bcd3a09166d72ed7fcdf29e6e88b1c3235c6715d"}, +] + +[[package]] +name = "filelock" +version = "4.2.4" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.11" +groups = ["dev", "docs"] +markers = "python_version >= \"3.11\"" +files = [ + {file = "filelock-4.2.4-py3-none-any.whl", hash = "sha256:301241438c57936ef93f4243a94f2559022268bfcf9b375a9a72eb0f8db8358e"}, + {file = "filelock-4.2.4.tar.gz", hash = "sha256:c6d8663535e1650ee430507b4ffbac7b464dd64b7647ea0112a9f406499d163d"}, +] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["docs"] files = [ - {file = "filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767"}, - {file = "filelock-3.29.4.tar.gz", hash = "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a"}, + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] [[package]] @@ -972,14 +1260,14 @@ files = [ [[package]] name = "identify" -version = "2.6.19" +version = "2.6.20" description = "File identification library for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a"}, - {file = "identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842"}, + {file = "identify-2.6.20-py2.py3-none-any.whl", hash = "sha256:6a16b69b93187244e0548cbfd25b3e4a6f9a7a2ad784625c3bec2b8d27b81aaa"}, + {file = "identify-2.6.20.tar.gz", hash = "sha256:ad729860a923858d26917c2f4fb0a1d83d27a75b1e090c06440c573f048f3285"}, ] [package.extras] @@ -987,56 +1275,67 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.18" +version = "3.20" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.9" groups = ["main", "docs"] files = [ - {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, - {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, + {file = "idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c"}, + {file = "idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44"}, ] [package.extras] -all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +all = ["coverage (>=7.10.0)", "hypothesis (>=6.141.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.16.0)", "ty (>=0.0.37)"] [[package]] name = "imagesize" -version = "1.5.0" -description = "Getting image size from png/jpeg/jpeg2000/gif file" +version = "2.0.1" +description = "Get image size from headers (BMP/PNG/JPEG/JPEG2000/GIF/TIFF/SVG/Netpbm/WebP/AVIF/HEIC/HEIF)" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +python-versions = ">=3.10" groups = ["docs"] -markers = "python_version >= \"3.15\"" files = [ - {file = "imagesize-1.5.0-py2.py3-none-any.whl", hash = "sha256:32677681b3f434c2cb496f00e89c5a291247b35b1f527589909e008057da5899"}, - {file = "imagesize-1.5.0.tar.gz", hash = "sha256:8bfc5363a7f2133a89f0098451e0bcb1cd71aba4dc02bbcecb39d99d40e1b94f"}, + {file = "imagesize-2.0.1-py3-none-any.whl", hash = "sha256:ea0c9a0384df69ed86a943a15cde37d0360b82491b3910dc2215e202e62b5b02"}, + {file = "imagesize-2.0.1.tar.gz", hash = "sha256:b2ba6a4dea487a7ebcd53248d3476aca449d30db12a2dde5e0c5ca9624fd77e5"}, ] [[package]] -name = "imagesize" -version = "2.0.0" -description = "Get image size from headers (BMP/PNG/JPEG/JPEG2000/GIF/TIFF/SVG/Netpbm/WebP/AVIF/HEIC/HEIF)" +name = "importlib-metadata" +version = "9.0.1" +description = "Read metadata from Python packages" optional = false -python-versions = "<3.15,>=3.10" -groups = ["docs"] -markers = "python_version < \"3.15\"" +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version == \"3.10\"" files = [ - {file = "imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96"}, - {file = "imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3"}, + {file = "importlib_metadata-9.0.1-py3-none-any.whl", hash = "sha256:bba5600596a7e21f3eef53281cf28d6a5195634d2f2b78ff9501a3272c6eaab0"}, + {file = "importlib_metadata-9.0.1.tar.gz", hash = "sha256:ab830580bc0ef3db61ce8fae716389e5462b67e033018bab6d8f80ef17172f99"}, ] +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +perf = ["ipython"] +test = ["packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.17)"] +type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] + [[package]] name = "importlib-metadata" -version = "9.0.0" +version = "10.1.0" description = "Read metadata from Python packages" optional = false -python-versions = ">=3.10" +python-versions = ">=3.11" groups = ["main"] -markers = "python_version < \"3.12\"" +markers = "python_version == \"3.11\"" files = [ - {file = "importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7"}, - {file = "importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc"}, + {file = "importlib_metadata-10.1.0-py3-none-any.whl", hash = "sha256:cf45ce66a929c64e994185a809c04840d0501db17e663810d993d0f4829265c7"}, + {file = "importlib_metadata-10.1.0.tar.gz", hash = "sha256:82b04047fa09792c1a8cfeddfe08bec79e4b13c29c8469b0d3949860f61017e8"}, ] [package.dependencies] @@ -1048,19 +1347,19 @@ cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=3.4)"] perf = ["ipython"] -test = ["packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +test = ["packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.17)"] type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] [[package]] name = "iniconfig" -version = "2.3.0" +version = "2.3.1" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.10" groups = ["test"] files = [ - {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, - {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, + {file = "iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7"}, + {file = "iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960"}, ] [[package]] @@ -1080,7 +1379,7 @@ pfzy = ">=0.3.1,<0.4.0" prompt-toolkit = ">=3.0.1,<4.0.0" [package.extras] -docs = ["Sphinx (>=4.1.2,<5.0.0)", "furo (>=2021.8.17-beta.43,<2022.0.0)", "myst-parser (>=0.15.1,<0.16.0)", "sphinx-autobuild (>=2021.3.14,<2022.0.0)", "sphinx-copybutton (>=0.4.0,<0.5.0)"] +docs = ["Sphinx (>=4.1.2,<5.0.0)", "furo (>=2021.8.17b43,<2022.0.0)", "myst-parser (>=0.15.1,<0.16.0)", "sphinx-autobuild (>=2021.3.14,<2022.0.0)", "sphinx-copybutton (>=0.4.0,<0.5.0)"] [[package]] name = "ipykernel" @@ -1158,33 +1457,32 @@ test-extra = ["curio", "ipython[test]", "jupyter_ai", "matplotlib (!=3.2.0)", "n [[package]] name = "ipython" -version = "9.14.1" +version = "9.17.1" description = "IPython: Productive Interactive Computing" optional = false python-versions = ">=3.11" groups = ["dev"] markers = "python_version >= \"3.11\"" files = [ - {file = "ipython-9.14.1-py3-none-any.whl", hash = "sha256:5d4a9ecaa3b10e6e5f269dd0948bdb58ca9cb851899cd23e07c320d3eb11613c"}, - {file = "ipython-9.14.1.tar.gz", hash = "sha256:f913bf74df06d458e46ced84ca506c23797590d594b236fe60b14df213291e7b"}, + {file = "ipython-9.17.1-py3-none-any.whl", hash = "sha256:6d1645743cfd1a07eb695d85aa2b5fa66721f8cbae9431d4049f7084bbf06509"}, + {file = "ipython-9.17.1.tar.gz", hash = "sha256:8919be8c27f20a6f4423145028063f6637b42a03ce57665bb12015ee1f073529"}, ] [package.dependencies] colorama = {version = ">=0.4.4", markers = "sys_platform == \"win32\""} -decorator = ">=5.1.0" ipython-pygments-lexers = ">=1.0.0" jedi = ">=0.18.2" matplotlib-inline = ">=0.1.6" pexpect = {version = ">4.6", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} prompt_toolkit = ">=3.0.41,<3.1.0" -psutil = {version = ">=7", markers = "sys_platform != \"emscripten\""} +psutil = {version = ">=7", markers = "sys_platform != \"emscripten\" and sys_platform != \"cygwin\""} pygments = ">=2.14.0" stack_data = ">=0.6.0" traitlets = ">=5.13.0" typing_extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} [package.extras] -all = ["argcomplete (>=3.0)", "ipython[doc,matplotlib,terminal,test,test-extra]", "types-decorator"] +all = ["argcomplete (>=3.0)", "ipython[doc,matplotlib,test,test-extra]"] black = ["black"] doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[matplotlib,test]", "setuptools (>=80.0)", "sphinx (>=8.0)", "sphinx-rtd-theme (>=0.1.8)", "sphinx_toml (==0.0.4)", "typing_extensions"] matplotlib = ["matplotlib (>3.9)"] @@ -1238,18 +1536,18 @@ plugins = ["setuptools"] [[package]] name = "jedi" -version = "0.20.0" +version = "0.20.1" description = "An autocompletion tool for Python that can be used for text editors." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67"}, - {file = "jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011"}, + {file = "jedi-0.20.1-py2.py3-none-any.whl", hash = "sha256:0fb16d86c4a4c73c37ba518c77419975e30fcc620658a8d14fbb5720cdd34142"}, + {file = "jedi-0.20.1.tar.gz", hash = "sha256:2f71208c3f9c1bca057c0e90d3f272aba44ace88fc4067d7587e9e069331b7e5"}, ] [package.dependencies] -parso = ">=0.8.6,<0.9.0" +parso = ">=0.8.7,<0.9.0" [package.extras] dev = ["Django", "attrs", "colorama", "docopt", "flake8 (==7.1.2)", "pytest (<9.0.0)", "types-setuptools (==80.9.0.20250529)", "typing-extensions", "zuban (==0.7.0)"] @@ -1287,7 +1585,7 @@ files = [ [package.dependencies] attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rpds-py = ">=0.25.0" @@ -1312,14 +1610,14 @@ referencing = ">=0.31.0" [[package]] name = "jupyter-client" -version = "8.9.1" +version = "8.10.0" description = "Jupyter protocol implementation and client libraries" optional = false python-versions = ">=3.10" groups = ["dev", "docs"] files = [ - {file = "jupyter_client-8.9.1-py3-none-any.whl", hash = "sha256:0b7a295bc46e8751e9adae84781f726c851c1d911bd793edc4a3bde942e3da81"}, - {file = "jupyter_client-8.9.1.tar.gz", hash = "sha256:a58f730dd9e728ba16ba1d62ebccf7ffe1ebbdbce4e95cfae941b7321ae1f4fa"}, + {file = "jupyter_client-8.10.0-py3-none-any.whl", hash = "sha256:5f73f24f22fa25192cfff6b23c051932a2473a797b05734aff495b392103e14e"}, + {file = "jupyter_client-8.10.0.tar.gz", hash = "sha256:9f7116294dca55f1785be880057d44544db9b1567718d92cb33c58886afb9497"}, ] [package.dependencies] @@ -1369,103 +1667,150 @@ files = [ [[package]] name = "librt" -version = "0.11.0" +version = "0.16.0" description = "Mypyc runtime library" optional = false python-versions = ">=3.9" groups = ["dev"] markers = "platform_python_implementation != \"PyPy\"" files = [ - {file = "librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f"}, - {file = "librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45"}, - {file = "librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c"}, - {file = "librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33"}, - {file = "librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884"}, - {file = "librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280"}, - {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c"}, - {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb"}, - {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783"}, - {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0"}, - {file = "librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89"}, - {file = "librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4"}, - {file = "librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29"}, - {file = "librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9"}, - {file = "librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5"}, - {file = "librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b"}, - {file = "librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89"}, - {file = "librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc"}, - {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5"}, - {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7"}, - {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d"}, - {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412"}, - {file = "librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d"}, - {file = "librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73"}, - {file = "librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c"}, - {file = "librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46"}, - {file = "librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3"}, - {file = "librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67"}, - {file = "librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a"}, - {file = "librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a"}, - {file = "librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f"}, - {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b"}, - {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766"}, - {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d"}, - {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8"}, - {file = "librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a"}, - {file = "librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9"}, - {file = "librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c"}, - {file = "librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894"}, - {file = "librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c"}, - {file = "librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea"}, - {file = "librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230"}, - {file = "librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2"}, - {file = "librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3"}, - {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21"}, - {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930"}, - {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be"}, - {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e"}, - {file = "librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e"}, - {file = "librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47"}, - {file = "librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44"}, - {file = "librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd"}, - {file = "librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4"}, - {file = "librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8"}, - {file = "librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b"}, - {file = "librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175"}, - {file = "librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03"}, - {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c"}, - {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3"}, - {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96"}, - {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe"}, - {file = "librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f"}, - {file = "librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7"}, - {file = "librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1"}, - {file = "librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72"}, - {file = "librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa"}, - {file = "librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548"}, - {file = "librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2"}, - {file = "librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f"}, - {file = "librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51"}, - {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2"}, - {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085"}, - {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3"}, - {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd"}, - {file = "librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8"}, - {file = "librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c"}, - {file = "librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253"}, - {file = "librt-0.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6bd72d903911d995ab666dbd1871f8b1e80925a699af8063fbf50053329fb05f"}, - {file = "librt-0.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ef69ac715f3cd8e5cd252cb2aebfa72c015492aacc339d5d7bf8fef3c62c677"}, - {file = "librt-0.11.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:624a40c4a4ad7773315c287276cd024509b2c66ff5904f504bfc08d2c70293ab"}, - {file = "librt-0.11.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:41dc19fe150b69716c8ece4f76773a9e8813fe3e35e032a58b4d46423fb8d7c0"}, - {file = "librt-0.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e8bd98ea9c47ae90b319a087ab28dac493f1ffbc1ecd1f28fcdbf3b7e1108d1"}, - {file = "librt-0.11.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84308fc49423ce6475d1c5d1985cd69a8ca9f0325fc7d5f81bb690a3f3625d4e"}, - {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ff0fbaf5f44a21beeb0110f2ab64f45135a9536a834b79c0d1ef018f2786bbfa"}, - {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9c028a9442a18e266955d364ce42259136e79a7ba14d773e0d778d5f70cd56f1"}, - {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9f1692105a02bcf853f355032a5fdc5494358ef83d8fd22d16de375c85cec3f5"}, - {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7a80a71e1fda83cc752a9141e87aae7fef279538597564d670e9ce513f286192"}, - {file = "librt-0.11.0-cp39-cp39-win32.whl", hash = "sha256:140695816ddf3c86eb972981a26f35efd871c44b0c3aed44c8cd01749386617f"}, - {file = "librt-0.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:92f7ff819c197fc30473190a12c2856f325ac90aabfccbeb2072d28cc2e234e3"}, - {file = "librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1"}, + {file = "librt-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:96f576f2711f8519152ec76d0e599243555c1f07679fa73606ca8c8c868c0be6"}, + {file = "librt-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bec3818c7da7c96ceae0ef5915a3d16c52dd08f3ea913bf1fe8568c447c7978"}, + {file = "librt-0.16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c79520414a3fdfc6aabd7593e6169afa14d5f8d9908d4b498db068868b08dd"}, + {file = "librt-0.16.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f81b5b19ce748ef68d4746656b7929762eb2fe99269b266e4be07e2ee4de7144"}, + {file = "librt-0.16.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c71d1b76210a36729fedfc5115069b50a3d8619054745f8758fe5d6f19e86671"}, + {file = "librt-0.16.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:473eebc7866bb0a0c8849a292b5e7157c1aba5d14d0f0f610c52158d6d964262"}, + {file = "librt-0.16.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4eb1313a19847089ee81e88742abedf285c60538816640b99742d8534b81d26a"}, + {file = "librt-0.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ebefd60b42e2a82b32d136bb5f7c94eadfcd29f772b547df6f3291d1ed855a1c"}, + {file = "librt-0.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6c5da27e8056439f927ea896735da60e616c477a8293feaa3233d4e7781a6726"}, + {file = "librt-0.16.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:67e718c7a43f8db325abbbf1404e2d535f12f8f7a1a82259568385cc5274b82a"}, + {file = "librt-0.16.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:314e703f0c19320dc8094e7a784b9cf29e1b67402515abf580069a6363c0b4f1"}, + {file = "librt-0.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:943c6bbecbdf7fa575a4f2952fcfd848c88ef95507c3fca411e89d4ac3ff8143"}, + {file = "librt-0.16.0-cp310-cp310-win32.whl", hash = "sha256:7cc365f006891afb006b52d5ee5ee74c09306ffa20e2f8705a32a4450af2f3ba"}, + {file = "librt-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:0314058469f4d2fd279ce7c62ac274ac82c3918ef7db62ef0697c4c359370155"}, + {file = "librt-0.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fe4372c52d4849096c6cc1cda2817d293ec51440c890474ed59ef38d46556f18"}, + {file = "librt-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c72c5295a84bd249526da9bdca38f2e176d15c31c13bb0063c5053f4ca023421"}, + {file = "librt-0.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be56ba9c884143495b517f23fe794ae367d58cd89ea0fdd6d437e3c024a87f9f"}, + {file = "librt-0.16.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:ef46c1a29ffb8c72e882e22618ec618778eacd0578fb22c6e7cf9c11d15f357b"}, + {file = "librt-0.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d3c94211ee0c4f8d649ec06b7c115c0ec4eadb873a0e3154ca15cef3f814b071"}, + {file = "librt-0.16.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94aed6a8308818b91677957d1bd03188869cd7aeb23c5dba7912a6c0402f7602"}, + {file = "librt-0.16.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:349c0bcb87ebd07481b6ff781e25cdc699723dbe2212e57dabb27f7a13b7b87d"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:001bfd59a7d45b17e3e75f2a8c6405280b35e7b84471792778e718c4f368950e"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:28e038895b998d7a0c7798922ce8a1dc157675df5cf1c9ef0aca809ed804b7a1"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0dbe4096a7ecc00fa835d24510ad8545a4efef738dac96e0e63516783ccde905"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5cd5b092441053364af968ea12084692cb9d4a22f3ce9524e377880bf028761e"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ddeb3c9dedb461bb457c6c7d9aa7fbf35329da313d1a7543d00c8d0f3473c96"}, + {file = "librt-0.16.0-cp311-cp311-win32.whl", hash = "sha256:e05108e0849966f53a8d2d3112a7af881d0efaa479bc735bba91108f9f2350a7"}, + {file = "librt-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:5f49cff01bd608ef7d97104cb035c75455e79c2d70bf4a506cf773338ac1860d"}, + {file = "librt-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:1d28ae980ae2218f9c5b95d191e947296f918c9bf0b400d467a9430275bbe678"}, + {file = "librt-0.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe52bf4641069e7978a14253b036cb9002def1926317e710f2e249f8a8c47742"}, + {file = "librt-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5bcc2c4726ced915b00de0c9856a4eeabfb3fddb93e10e0b8f735b7709358b6d"}, + {file = "librt-0.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff7baa55f8e7c69851419e50a666015d02a74198716fd45c0125a2112e0a389f"}, + {file = "librt-0.16.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b95d5d92ab83d39e760a52091bb1baba664f3a2351e39b1e16801e5747c2f0e9"}, + {file = "librt-0.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6d085d70bce51d43c5c7c36d63490770180d8779e71c49305c87b4213918de7"}, + {file = "librt-0.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36e53948e99bbe3ffea257124cfcae1cfb01831555c9a9c903c9f9a72db7fd07"}, + {file = "librt-0.16.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:54d11f726aae9df5a6ffbbf0a03a52449bbac84a53ef03669cb41cdfd4ae41bf"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4323193ac0cd025f85af531df8ba91bf24d1973b401697347a6282e8fd3fcf5e"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e42f8e098b9c5396fefa05fb1cc7e33b0e08fc51da106b5de4a45fd22aac6743"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:39ec1d5a14e37baf1450a6cabf03fe552340808bf1ad9d71824ab90117716459"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d1aabe3925cbb4a08d15b7b20ba4011b53019da0c4173a25155139b7b1baed65"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:300c3ffdc459f4a779a8411ecb188e3ac0b1ff3a3a7b099642555dedae06c69b"}, + {file = "librt-0.16.0-cp312-cp312-win32.whl", hash = "sha256:c17194318e4c0c0348b36f36c2ec7534436fe0a4c15582403162a4f08c80797a"}, + {file = "librt-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:25a58a19ea8d83b68209f04912df765e9260635ef77646542ed4b4abe6bc7940"}, + {file = "librt-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7be7cf555bc30ec12622e9447299cc4a9b8ff307548b634794353db0c2065dc"}, + {file = "librt-0.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c5e6144e68b577f157519f2ba88ca20e3ed61c29b00e5cdfa76cd2d45acf059a"}, + {file = "librt-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:33f41443a1f4e1f099331b3d8120e409fbff84b9760bc1cc9ea496f37ddaa5cc"}, + {file = "librt-0.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e510b7770bee609617a3374a96548eb114cae048023e3f049ee449e7ff2db32"}, + {file = "librt-0.16.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:efc49c462d4516b8a58b00b490078fa64689fd1fe66970cc190131d7afb8027e"}, + {file = "librt-0.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:92caf82ebef5e12d21c72242b70d1e92536f1711cf2a727a4c276de4b4469087"}, + {file = "librt-0.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17bac7f7a16b328fff77e440287693eb017abde913595b5827ebccbc21ecd8a6"}, + {file = "librt-0.16.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5b976054553670829985ed767feb78fb6bcede0175327c4844dd5c281c1be659"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0058f9d68721094105917254c72ac0569117bb7b13b9769cf45d26d89f9d21cd"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b7beaf3f4487b7d8adef1f158b49067cb4d5a19fa7a3bf31a4e7a820e435c5"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:468df902df016a06eb0e40b0747dc8d14e47d7a38b18b63b1fb167d85cb94d63"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:aea7b1f2b125dad5de85f049136651bff256c883c65e6b9209b2da0a1ac3cdef"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a8afb6557920860b7a3a596eb804cf37e09e7cf8a803db2478c202acc72d8c2e"}, + {file = "librt-0.16.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:77c7a2b4fe2c1369e0d5aa1cade26740a7b14be32fbc9a5535d617d20065c39d"}, + {file = "librt-0.16.0-cp313-cp313-win32.whl", hash = "sha256:02d89c813d5ff74b17df72d3a34819d132cd168e56b81bf755b809bd9e46b8c4"}, + {file = "librt-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:14ed6ebe3e4f85f326d7920011ad30ff49ed9334e62cf88caef9ba973d9e3a92"}, + {file = "librt-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:83d4041a3d9b2fd053a8a4e1f22878b3e5833e2712956382d5c048d791454e91"}, + {file = "librt-0.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:931a0bb0fcac88f263e269e46eb30ba8e21402cd3c62ca40cb97034c0693fab1"}, + {file = "librt-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1bc17e54e5305f8d40b7ca203671ff5a9e59c1d0f8ea0f625dcca53a3984de11"}, + {file = "librt-0.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:877698bf6bca5721d8be345f2fe09778e40ecadea8b58c73075f2b1a53666bf2"}, + {file = "librt-0.16.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5981c011b306781ce561e18e14230a14524a3d8109b97553666c942c18f31a96"}, + {file = "librt-0.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:afced3dfc17cd805ecf7a3d77996a71cf5f2c75aa66eb0c21a9930f4fc992f86"}, + {file = "librt-0.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca8052401c55d7511dda6760719fda7618067e83535d7d0010096d216c34b667"}, + {file = "librt-0.16.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1e511762a074005bb0aa569166779834e75e438370226930d0ce1866d4b6a33b"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1e8591bd8a5a628cd7f07954c6a1592359a878bf032957a8e9057a41d644311"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4aaefb4ba6c07e1aeebb2795c8958148f1d6f9af3b555b53d23d766edb6d67a"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d92db7a0f6aee44f1baee94750457e8d2d1c6ccea41842de6268d34e8dc7eddd"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:378dfaffb38e59c24a87cde5713cd865d51ff7383fa12947f3907f306ea1ca55"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e0c39bdc85370422e8b637be76eb1fd07d30967551b03e62267dd156f553152"}, + {file = "librt-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:1b384b90ab79a7bc30b566895809a636e0666f21f3cf12b54823d025b7e83839"}, + {file = "librt-0.16.0-cp314-cp314-win32.whl", hash = "sha256:52327da75a94012e7f932f913d20d3876bed3c102be00e6c3e8600ff7bdd58a7"}, + {file = "librt-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:3f0b8114c44b2ac06ff5dacd08e07e8e807ff4f46083f2a1602685122559be41"}, + {file = "librt-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:8caf96a4ef8fb27d0ac0d1ad8337d26a240acd4a02fe4345d0a8f264753e8f99"}, + {file = "librt-0.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:953107e2f68d0f3512c48f898b0dbf0ce5cc52bba0f318d847c985dc555ee4cc"}, + {file = "librt-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad37d5b9abd49c9a655dcda7ea52a8a752884062ef1ee71ae17c2f2a0f81fe6a"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c4aa329c17bd1aaea4f6e89335d8ccd494b3a5830b6654462273e50e11023f0"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0ead24d2562a49473dddd9efef8581f020007eb0054389c3ee3ffad38b1ca4c9"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02118f56a9c36ddd07dfd9b919d9ecc117ba20a90987d56aa4c429fa34509188"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e29522c62e28595ff7e324c6834ade51127707f0e255b18d1c1cf03d39c1048"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ff4b2367926b69c6215635902cccb04048e73094e9862900d27cb2c6bbff143"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c6f1b27bf1632a7e016af9f145f82be95e1edd7721a646505c21059257cb5a04"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5696d7f52e7b37217cb3a8f92c744fe835942602fdd4c1a8bc4741d3bfdce15e"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6072e92dd876ff6ceeb6cf371e35e51f479349837391341f479b08df4564242b"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:39ca4f2f2fe05de8e63493da592d84311adabe5bef52b193851981da9816b302"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9807485a908f00355820f18e91e045ffdcdc5adb68aaec40a1e2b88c5f7bba1"}, + {file = "librt-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:94be5cb7bca4df6201f4183e9e4fa2086c655283d20b38cd84500a69057575a7"}, + {file = "librt-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d46ca272b251d033dd4527b0dec5f261a28a52bd5fa0f99c117b0a1f8588cc2d"}, + {file = "librt-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b9d6d4b14e92d876f8026b54c20c445f36425214c1081dc76f74e40db386b82b"}, + {file = "librt-0.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:6fe436af2eaf630474f491af5d032cbe45f93fcff5c3b9fe4ab194a7255b20ff"}, + {file = "librt-0.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:8ff5d26c529336be9bd7ae04483235d77778ee7d6444a95353102b542601ce81"}, + {file = "librt-0.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:909d8e3c1faee44cb762b1c519ff8613dcc5ceae5c99987a00917b5a31fd1d6a"}, + {file = "librt-0.16.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:6d4a64283ee61824b5790de882bc68e2d9d7a5143537cb7a966f7354f71646d4"}, + {file = "librt-0.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5810ba811297fdf37a1531a57667cb8ace0842013ca8606bf9eb7c24cf4be154"}, + {file = "librt-0.16.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e56aaf8c167548dc8e5d6f3bd0f48dcdd299a23c73be3f744aab79d99e9c7f5d"}, + {file = "librt-0.16.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f36c58e33b304b525c6c9c5076399c6ebf1109e17b9051a05a407b091b9215b"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:242e00b3d4fa37c3d3c1ca5f5c9adb7d909ddb1eac9c41f2787320d00caa0af2"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:0253721561787b8df8443eb347b7a6461015354e5bdd37ee38a41fef220d2bb0"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3e483a8d69ede8067db70c0e83007423b6925de6fd53afed01d66160f2e9398c"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:69ba927445cfaaffb4081003ef5224c55a5c2ab67ef956f416ef744916e44121"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d6a365f2ab45a984d0e00eee0dd17f599ceab8cadab6ea07b6111c8132fc0e42"}, + {file = "librt-0.16.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f01f3805f2dae4781c0c34b440e31740d082950bdaf89a6f601ad589a28af57a"}, + {file = "librt-0.16.0-cp315-cp315-win32.whl", hash = "sha256:b0e3e721c75d2e79a76d4422c79d7ba705fe1bbafec907037fe7a657a480a0e3"}, + {file = "librt-0.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:bc02954b1295de798bbdb0b4e2d8a28c2117de8b5c73dcbeb27dc32572dfb971"}, + {file = "librt-0.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:c5db585d43449a5f54303d4b2774e45e1babd975cfe1630a3d708c0b80c3e560"}, + {file = "librt-0.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f06c689cb14afd9b612727553a5ec5a40febf113ca41c4413a2b0b334285884b"}, + {file = "librt-0.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:13b4e8aba90b0b1c82474e9844aa9ffe7ad3faa484350e1da64cb8188d903134"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a269c46ae327d8e6f8c1f85f7516cb52c0fa48127565a1105a4f4a05ff2a0b4"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a33e0dae1f8592146a4764d54ce842b278732d21a84e17c3bbe6b1bc158a2248"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47ada6ea32636492c61aa8ad27ae3b9404bfe7a97e3ba946d1984236cc741da0"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43bd6e642d8a248c114327f98dd25ac5a7cb5aa168ef02f0559b91874df16b8"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3d1bb7841a816ace6449bb26d3f9560dbfa20e71c568d23f0f62bf1e68f50b1"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3931f7a3db322e7f44e02a280e3949326ce9579ad388ee8d691dc7c76da9fb70"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f4462528b6000afe8f16907b5c7c2553abf1df005ba5140e6eb394541c3624c3"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:80039ba9b6a7d5f1a0175a4cca6bbefead87bd854c80abad1cb30afe47a830db"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7a1d272724b581bb6bc769dfdafed6da2ecc9886ba2450311de55a4ac2e1e9cd"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:fbe4fb8c5445f7496d7f7f6bb0807875d09d47e6771ffa175fb2df2895fb86ba"}, + {file = "librt-0.16.0-cp315-cp315t-win32.whl", hash = "sha256:375bfe6b572a8f6cfc398709356046173bf27e64c4c5edaf5f7062f051fb4bf9"}, + {file = "librt-0.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:bd3150023d3dc2bc70f3784e59ffa1140d56ddba3d8125b3d6f9f85221279bfc"}, + {file = "librt-0.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866"}, + {file = "librt-0.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e1967e36ac4cae0c7e9615ad32e1a513cdacff79f9e8afb28bedc91caf48b4f3"}, + {file = "librt-0.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:20fe0bf9053885e21c62b3e091fb5e73e1c54d1daf2e70eb388d70763bcd4220"}, + {file = "librt-0.16.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a63610fa76524edfa605b5b259a603915c7a6e10e54f003e5503030506e81de"}, + {file = "librt-0.16.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:845a511b60ca43b9880dcc84a9784c891d6a2098c829130b320846c69c9c0c68"}, + {file = "librt-0.16.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d608f0bf3b8cbddd0067fe02cb8cab7d13e8eb9386b23a1843d4363044fd9e22"}, + {file = "librt-0.16.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df183721229ae51eef90108c115b43e98cb169b6155d34480338e5fc6616df00"}, + {file = "librt-0.16.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b6183e2e2e0ee00aac2ec07c7f7d151c97e85666b304f574b71cff0f9fccc4e"}, + {file = "librt-0.16.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:273d00be33792a15189331df10f1f1331621b043881e66c6c4377f7f776e1291"}, + {file = "librt-0.16.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aa9357a1b4d4fc787bb718a59cb1112c28c8a976d6bfa268b71cc0a4ab8f3a94"}, + {file = "librt-0.16.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:e9ce0bc440e7fd09b5f51f372f0f6640658f854b8fb05260f819cc669c93c42d"}, + {file = "librt-0.16.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:6c8893eae2fd13c5488d94056f3e6e5cf3142bfb1c4acaf136cb33d760c5964b"}, + {file = "librt-0.16.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7393c9a48dcce4817dbd4b0d8ff6237efe9b0a0609f5b0adaef315f8541726b5"}, + {file = "librt-0.16.0-cp39-cp39-win32.whl", hash = "sha256:71b93b42784e25b975079573c642a8fedb049a7bb31d70a51721b1666b3b2ced"}, + {file = "librt-0.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:5750a105b42a416f930edc59054927a406effb2550cd5bab92ad7a5842ed5d05"}, + {file = "librt-0.16.0.tar.gz", hash = "sha256:ac38d6d8d66bf3d744148dbbc0b8e193e195a51e364ed55e224631f5721891fc"}, ] [[package]] @@ -1495,101 +1840,163 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" -version = "3.0.3" +version = "3.0.4" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" groups = ["docs"] files = [ - {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, - {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, - {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, - {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, - {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, - {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, - {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, - {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, - {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, - {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, - {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, - {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, - {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, - {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, - {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, - {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, - {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, - {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, - {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, - {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, - {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, - {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, - {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, - {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, - {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, - {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, - {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, - {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, - {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, - {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, - {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, - {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, - {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, - {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, - {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, + {file = "markupsafe-3.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd8ea6ebee7aedbf7c749fa80521d9ccf1ba473e0d1e14805caafbaad281c889"}, + {file = "markupsafe-3.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dff05cb7016dff1e9fd68f4122c127b65dfc59de5306cfb7ad92f956f230bee2"}, + {file = "markupsafe-3.0.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf63c214fe879a65e69a386f915e36104fc84254ab141240f8854602d8e0be2a"}, + {file = "markupsafe-3.0.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2a6ef68ae94aed8721934072b27a3b654ea2100b97e4ab864cf1489c90926fbc"}, + {file = "markupsafe-3.0.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd9f8797427910198f95bced71ddfed61130d7e349213bfb8466c9c99e2c46a8"}, + {file = "markupsafe-3.0.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1aca03ede943eb80ab3d63bb082c84b7aab85ea83bd0fd0c200260945fb49d9"}, + {file = "markupsafe-3.0.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0764a13d34cae40db7bbf3a09b7e9b491bf4603e20b263a7a9d6b8e324975d0a"}, + {file = "markupsafe-3.0.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9388003072b95f2f1e3fd908604194d653ba21330d811961a78b7da1a77e9e36"}, + {file = "markupsafe-3.0.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:8698d70a8081ee8c090dbb394768b5789a1da8b131b5499f89d071dd3cfaf6be"}, + {file = "markupsafe-3.0.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:bf053da3c97a4bc5ecfbb218cdd2983febd91c617be8367d139882aa11e490aa"}, + {file = "markupsafe-3.0.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:9438a2648b2195980cb2dd8e53ed7b8df91319e2d0b70ae61a9e1d1bc8d3bec9"}, + {file = "markupsafe-3.0.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:88d59b473bfb03259722600839af9bbd7fa13a2eb514beefeedb95997882f69a"}, + {file = "markupsafe-3.0.4-cp310-cp310-win32.whl", hash = "sha256:4a540e2d3192792fc84eced57bef37851ccb2b41f73291bb17408eea77bcd278"}, + {file = "markupsafe-3.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:5c22873ad1f0532ba40fa1727f3c0fc1bbbaab6d373d4cbe3f0dc74b2e2521c7"}, + {file = "markupsafe-3.0.4-cp310-cp310-win_arm64.whl", hash = "sha256:3d23795802fc8bd72534836d64489bbf0f67c088959091bdb22e10735a5107bf"}, + {file = "markupsafe-3.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e25feb9e330b63edb0278a0acdf85e50d0cb0fbf49c3084abbe4e24ae195346"}, + {file = "markupsafe-3.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d3391b2188d18737cb2fa147028b1096236eaa7e156446c650a489fa2cadc91"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:849dd2bb0e5e4ab2b71c7191726a4a8d5aa8a610daa584728cbee0b710ddc4ef"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:befb4158af32106b9a93db8d6d1d1cbbd418c0d5aca0cabb7b1780abf0c89169"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71f88e749ea29f67f21f3b36433c1dc54c7729ed2a6d9e2da2e0d9e0d7b224eb"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6da83a088f8ef93b2d483a8232a4dbf4d69d3d8496b568a03c56becac43e1808"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f0fac8b13d14bb06c68195f849371924ae53dd7b1c00fed24650f704383b692"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4a7cdc2a420ca01058182da4253329764d4bfa055564d1eced90e6ba1e8b1d3d"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:83b3944fea42a8400edf92fd1770fb8d0d4f7de651353bd2d8525a92dba69a21"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8138eb83940ec7299024d92d4dee45f601b9e6c5ffde9d25f4e35e326203c707"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:811d02d5122171c1941357efd8f9bf4ffe907b7f0a1a4e729a880e4be3f46e3e"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:50b5bedc9ed8a94fc8857a42ef4f84a81ea88f8d4f05dc8705fb23ee6d8dcca7"}, + {file = "markupsafe-3.0.4-cp311-cp311-win32.whl", hash = "sha256:2e5a7cd7fdd14fcb1ae5d7d8bf23d24fbd1daefd1fbca2580132e1ea75f098b5"}, + {file = "markupsafe-3.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:fdb4ca07ab75ffadab4a8b135ad59cdbb3156b99310f3d565370da74a15d6bd3"}, + {file = "markupsafe-3.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:569d65055d367e3dcdf30c3f41119467b73d9ee9faf332bdf40402644f5ac08e"}, + {file = "markupsafe-3.0.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61631e08084be9e21a8967ec3139c7616ed7c5e9368e05c86d1b39562c8a57b6"}, + {file = "markupsafe-3.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0930db9bdc62d22944e10b066448bb65dc9abe9112880c7cab8da54db4284d5f"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a45c3d514f2436064db00d7fc8778d888f0236ebfed649b53d13a59e69ad51b"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1e1451fab512d1bcc3dc26988ec1edb0b82c2db909132872cd9356070a6b63df"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bd3ce56ae2cbae3ba82b683bc425cd7e48d2ed8b10f3e818186b6f5646d9271c"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e124f974786f831d6043728e38296969d3579db8896fe004682f5758e613581"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c02e8f18bdedba082cef725942ac823b9b60656db07f7e265cb31618dfd00d77"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f098115c247e11d138ab83a28fa0323c77015007ea2df73ba5fd714dfefd67c"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d5f93ebbeb8032d47e349328ec8662d973d9b05a70b3c35df1f91fe419b84749"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:64511c54db4e4987aef4c41923235927428729e8174c5dba488429be70a998ed"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e1a622f13970d81f95d0c72f9dc090dce9085fccfa4c9f2174377ee32bd15786"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9a7f43c0b202b334cc9184af09bb8f21d3a209e038efaf106936fb69e6b026e"}, + {file = "markupsafe-3.0.4-cp312-cp312-win32.whl", hash = "sha256:f0ec3b750b59375eab5b0fb2b9254810c00a3375be6d789899f1055a1d556237"}, + {file = "markupsafe-3.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:11935df9bf455ed0c04eb87bcd720f02b1fe5e02128a9430f23aed6f93336fc7"}, + {file = "markupsafe-3.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:a4bbd2d87dd233b9fc5812160c3d0ffbe42edc22a26ce0469f58479ede633fe9"}, + {file = "markupsafe-3.0.4-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:de8b364c423ef0a4bad9069657d617f9a5d2b2062457a89b1fa16ee199c399c1"}, + {file = "markupsafe-3.0.4-cp313-cp313-android_24_x86_64.whl", hash = "sha256:34bdde374c5932765d7dc685c4a1d191a3207852d67e8e0a9eb6ea85156181f1"}, + {file = "markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:6bd9e1788e15bfcf6a9082de42e30387e7b85d211ab21e57a939bb8cfaaf8d96"}, + {file = "markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5066b244f576f91afc8ee3ba029a89f99d39c79b1853fe9d39bea9f0afbec148"}, + {file = "markupsafe-3.0.4-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7a83aa6e4805df46fed18e989d3d16f86ef60cb50bbc8d9ce3a6be89165fbf6e"}, + {file = "markupsafe-3.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2d1b7d9308288661f56672b1b157d75fc536714d3638487bbea17b6318a78248"}, + {file = "markupsafe-3.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73e77980c7207854f00fc4e71fb1626868d5740ab4012623d55c7a99ad122a72"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7018d4af1cd272e847aa5917983ab5e83e4f6579f9dbfecd4a79c0ca80b144c2"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c90d5b3d4e944e065a301d741b3c1d784f6bd1f503aa68b4967e32b2ba313d85"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18a801868a884f216e784d7d14db2a4077143ce7610440aee2ce8f734e7cfcde"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434139499bb20b502ed3baa1f169e618f924a97e7a777fea1a49446d80106cf6"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9e227f3dbe6bde7491cf0a9965d00b88c6b1a4a95d11480ddf88bb96d397c19f"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b8cd1f918b26fd7b1832ece557cc18f2d8747309ff8b3f0ef9d4250c5ad67a39"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a5fcffb37e602b0b3c1638a97746b9b96125caa9bcf6fa41d337a9261de231ee"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5989cb26b2e1efc6a42216a9f6b5ee495ce5ace2e5b352a9af489976b32d1ee2"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:add96447a86d205ab616665d53b2950ee81083757f56e6ea833c8b2917646b46"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2628d3a8cb648ecebb3c5d6b0a1052d400e4d8b7ac0fb786be8d285b50040d17"}, + {file = "markupsafe-3.0.4-cp313-cp313-win32.whl", hash = "sha256:672d207103e6b16ca098611b0f9efad6bc00afd47c03d6ef62186495ca677dc0"}, + {file = "markupsafe-3.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:1f1f9477e174582b0a1b583d60b66e1f2cf5d3fe12cee985e4aedf44766600e5"}, + {file = "markupsafe-3.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:06de8ef6331f6e822c28d577dc8bf43fe398800477c49498f38fc38b67ff33fc"}, + {file = "markupsafe-3.0.4-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:4ed644d75aa94a2baf7ec3a96eaa160ea58c742eb9d27c6506053c5c40fc84ed"}, + {file = "markupsafe-3.0.4-cp314-cp314-android_24_x86_64.whl", hash = "sha256:6d2a9efe686f9de00d0d1ea32a4a5a86d558a2277501bd78d964214eab625e59"}, + {file = "markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8781a792a070cf2bd1b86d3aa943894115faaba6e88122a7bf32d62072742453"}, + {file = "markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:971a3bbb75d97ae4e2e8f7d4834236f86f85f0c85e04ab2e191db1123b04f80b"}, + {file = "markupsafe-3.0.4-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:8909c2f1c6dd65e054ac4b573a91c8384d1492281e55d82d159d653f7a13adf6"}, + {file = "markupsafe-3.0.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cf3468d5ec187ffffcaca8e61929a37448f215dafc1386a12c750a72fe53634"}, + {file = "markupsafe-3.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:52704c5d36eb6dda8866493decd61111fff86244c9b1ad225ca01b9e91e5970f"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1caa2fa5a6184fb233153b35f654e6687bd555476f6170f29d8ee9be1a8b0af9"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:387d8cd30e69b3f0a72877b9ae717033396404e19095b17fe89753a981fda44f"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:051417f74bcaaefa316276e0ff723f541616ca51043d070da00249d9bddd3e3c"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8e9f292fcda89b324f2f5c91d13f1424a153e40fc2756f38ee23b15835ff300"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:df1ae86ff54725a01fa1a0510b914ca53a161b7050be74f6204e24aded5971d0"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8965520ac587c94a4ac48b729be3d8b8de00af39699b17585dfb599babe77977"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:340cbb1957ba99929cbf19a75626d36ba1ae21d1730b287d1cf7f824a20c4fc7"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3a93d9616ddecfb393727a0041a562cf0b15a244e20f2bd25efc7949be4c4f17"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d2e56fd3b00222722abfb3f5f0759ddbae4b90811b5ad4343c64030ad1bde70c"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0d9c47709875fdb321452056622e930c52afbc07a7d780762fbb8b4d91ce6fa4"}, + {file = "markupsafe-3.0.4-cp314-cp314-win32.whl", hash = "sha256:38fc55594dab834470b6733dead2ee9e3f657fb0608c769dcafa0ba5ab52f45c"}, + {file = "markupsafe-3.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:c1bc67752d5f21013cfe430df4062441714eab79f65a6a05e01505957e9c35fe"}, + {file = "markupsafe-3.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:7e1636da3d8dfc220b6dd10264db5f2b165e4888c4518594898fbe381049af8a"}, + {file = "markupsafe-3.0.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:805c8b84534fa10891890f0e4be39f3a99e94615d93e8836bf9fa1fdca2feeb2"}, + {file = "markupsafe-3.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa95848c929b6a75f6848d3c9793e59db365ee436776e57db835cdbfa79ba977"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e916035e3e9930cbdfdd10abf48861340221857f45509565898e012263f7b289"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4d12837e0203bbace818ff4a7461afdcd78bcd782351cea148139180d7bcffe"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5086f9975abb1ab531ee6afca1761e4b59a19b446f3f6522ed776963228cfe5a"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4a635a0487774f841cb1fb62e907e7195cc95bc761e053184b8acc3ceb20733"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb96e6e088d6cf71c1ea977510948320234824cf226e32f6f6e044f7a9c82b34"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8b5d563170ff8ba3181caa967c99a3c804d1dedb702c7cb93a6a7c32247da978"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:396ec4e65cc889f69786b3b89478b471cee5a3bcf468b9d9bb03e1a30fb291fc"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:15ba9e28640feef770374b116a6f019c21f52404aeabe516aa7f800587b98cfc"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d920abdfa61279ba1a2ef9484aab07bf03331f8c08a10120fa332353d06e6932"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a9f54054101545a9a9cccefddf54316aa6e4491611fcbef9e91b3b6bebec04f6"}, + {file = "markupsafe-3.0.4-cp314-cp314t-win32.whl", hash = "sha256:12a606a492de952afcb43b59a14aaaaad120e708d3663dd0fdf2d738d427a691"}, + {file = "markupsafe-3.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a18f38cafc329bac5e3c2b96c765b4c96d3d103421ed22ab7988c1e3fce27464"}, + {file = "markupsafe-3.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:eba154571c16e032112afac0dc2dfe9e63c2ceb7aedd07bb7eecf2ce26d4dd4c"}, + {file = "markupsafe-3.0.4-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65"}, + {file = "markupsafe-3.0.4-cp315-cp315-android_24_x86_64.whl", hash = "sha256:489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163"}, + {file = "markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92"}, + {file = "markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a"}, + {file = "markupsafe-3.0.4-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429"}, + {file = "markupsafe-3.0.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8"}, + {file = "markupsafe-3.0.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741"}, + {file = "markupsafe-3.0.4-cp315-cp315-win32.whl", hash = "sha256:03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b"}, + {file = "markupsafe-3.0.4-cp315-cp315-win_amd64.whl", hash = "sha256:d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214"}, + {file = "markupsafe-3.0.4-cp315-cp315-win_arm64.whl", hash = "sha256:353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67"}, + {file = "markupsafe-3.0.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad"}, + {file = "markupsafe-3.0.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894"}, + {file = "markupsafe-3.0.4-cp315-cp315t-win32.whl", hash = "sha256:e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78"}, + {file = "markupsafe-3.0.4-cp315-cp315t-win_amd64.whl", hash = "sha256:4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c"}, + {file = "markupsafe-3.0.4-cp315-cp315t-win_arm64.whl", hash = "sha256:39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba"}, + {file = "markupsafe-3.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f291bcf42ae98eb5107edb162c3c998b4a89648fd8e99ed4cbd12705292788cd"}, + {file = "markupsafe-3.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ac0c7c9f1609b0c4c114feb1d7a3409564c7fb77e360bed9e97e5d25dfeaf868"}, + {file = "markupsafe-3.0.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6768d67d1bce64270e0fdc2e69309d68b9b18ae56ddf6c711d168e9d051c2cac"}, + {file = "markupsafe-3.0.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:14bd2d845d62ab678eaf81da89d7b621b51756c72346745c1a594c09d49207a2"}, + {file = "markupsafe-3.0.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:007e1ffd9bf65bb6ee96df7b258fc632a4868dd5566037986c64781f35a36e98"}, + {file = "markupsafe-3.0.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e8b3d0b18fd623afa12ecb2ce8d8becef69f9b5440c6330c7972200e0bb84b0"}, + {file = "markupsafe-3.0.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:57f9947a7e57a081c1e3e0a2dd0d2dcf290a4531450e6f611e30084c222a7295"}, + {file = "markupsafe-3.0.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b61687d0828e72bf5cda24a2690188f37170bd31c9359ac97e4e66569f120a16"}, + {file = "markupsafe-3.0.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:0cee7cb0f9a1b6892ea482237d9403b3d1b4603aee057d0ff01f0fac2d019a97"}, + {file = "markupsafe-3.0.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:94e4c421742086aeee4c32a506eec8859d7634aad943f7e6aacf70f813478768"}, + {file = "markupsafe-3.0.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9240187afb63d2f9ddc3e032c670356fe941f6e20662ea168a5dc3f1f317e1b3"}, + {file = "markupsafe-3.0.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e841068dc0be4cb6dfb5c890eb88cbdcff2f4a332393c7ec94e8e618bd32c1a8"}, + {file = "markupsafe-3.0.4-cp39-cp39-win32.whl", hash = "sha256:f61efe1d2fe0de16158a5fe1d1cf3c14bdb6aecd54d8938fd26512c525c1f624"}, + {file = "markupsafe-3.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:2b2b1e18af909b448bb3cf9e3433366f7a8726271fc214e8b10e0f62a78c724b"}, + {file = "markupsafe-3.0.4-cp39-cp39-win_arm64.whl", hash = "sha256:6669c1bf34080161ce49c589cc512ef24d4c704ac9d2b2d3667f519c60418378"}, + {file = "markupsafe-3.0.4.tar.gz", hash = "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6"}, ] [[package]] @@ -1656,14 +2063,14 @@ files = [ [[package]] name = "mistune" -version = "3.3.2" +version = "3.4.0" description = "A sane and fast Markdown parser with useful plugins and renderers" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["docs"] files = [ - {file = "mistune-3.3.2-py3-none-any.whl", hash = "sha256:a678a56387d487db7368ede4647cb2ba1deff22ce61f92343e4ebe0ddfce4f2d"}, - {file = "mistune-3.3.2.tar.gz", hash = "sha256:e12ee4f1e74336e91aa1141e35f913b337c40bdf7c0cc49f21fb853a27e8b62f"}, + {file = "mistune-3.4.0-py3-none-any.whl", hash = "sha256:d03cec4c8620f21517a996d82d14f3b59c30d79bda2cc6c56bd32b839f18ce6b"}, + {file = "mistune-3.4.0.tar.gz", hash = "sha256:2eaf7952af928ca8d33b779762effc1da517b62c6ad85ec8ca957e3b07b9782a"}, ] [package.dependencies] @@ -1671,137 +2078,170 @@ typing-extensions = {version = "*", markers = "python_version < \"3.11\""} [[package]] name = "msgpack" -version = "1.2.1" +version = "1.2.4" description = "MessagePack serializer" optional = false python-versions = ">=3.10" groups = ["docs"] files = [ - {file = "msgpack-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c"}, - {file = "msgpack-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895"}, - {file = "msgpack-1.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203"}, - {file = "msgpack-1.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73"}, - {file = "msgpack-1.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833"}, - {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8"}, - {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7"}, - {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce"}, - {file = "msgpack-1.2.1-cp310-cp310-win32.whl", hash = "sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74"}, - {file = "msgpack-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb"}, - {file = "msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22"}, - {file = "msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5"}, - {file = "msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06"}, - {file = "msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4"}, - {file = "msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8"}, - {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b"}, - {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e"}, - {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f"}, - {file = "msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d"}, - {file = "msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8"}, - {file = "msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66"}, - {file = "msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35"}, - {file = "msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c"}, - {file = "msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0"}, - {file = "msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a"}, - {file = "msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6"}, - {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a"}, - {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1"}, - {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64"}, - {file = "msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac"}, - {file = "msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24"}, - {file = "msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07"}, - {file = "msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064"}, - {file = "msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056"}, - {file = "msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc"}, - {file = "msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d"}, - {file = "msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155"}, - {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402"}, - {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c"}, - {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6"}, - {file = "msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707"}, - {file = "msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9"}, - {file = "msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a"}, - {file = "msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d"}, - {file = "msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7"}, - {file = "msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889"}, - {file = "msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720"}, - {file = "msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190"}, - {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d"}, - {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24"}, - {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7"}, - {file = "msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb"}, - {file = "msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b"}, - {file = "msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7"}, - {file = "msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273"}, - {file = "msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1"}, - {file = "msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc"}, - {file = "msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde"}, - {file = "msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4"}, - {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d"}, - {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355"}, - {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c"}, - {file = "msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1"}, - {file = "msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2"}, - {file = "msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107"}, - {file = "msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647"}, + {file = "msgpack-1.2.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2a4e141a9c91d1502cf98f9ca9f87af222c7857e2bfa49c1d81343157c38a28e"}, + {file = "msgpack-1.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0626ea840cce866237c4da08439e9faa9cf79e6e4c62358978ee055961e8cff2"}, + {file = "msgpack-1.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25e1f2466c4ea0fb9b694a922f75eb92d7ce6262eda8cb8a5dada34fe990d486"}, + {file = "msgpack-1.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85b79ed643cfde70672152b3d1aee438ca647c5d2ffb7dba7226689ee9f6f44b"}, + {file = "msgpack-1.2.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c1119e840d2c9164120f067740d99c9608d21d4ee9a172cf0f00dcafdf5624a4"}, + {file = "msgpack-1.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ce22584c8b12aa7e424dab08c481afd95ad4bd3007829fd1767d28b4dbbc3167"}, + {file = "msgpack-1.2.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:8ab62a50cab6f786d49f3584914c1088b0ed8de76bfe675b26a60c7bec426380"}, + {file = "msgpack-1.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:429329799c72335b0e37be65964ec5b89062e57fe7ad9ee779b886df5e34ddaf"}, + {file = "msgpack-1.2.4-cp310-cp310-win32.whl", hash = "sha256:1cdb51af4f8f8d20af66ea9384883bf99b8ddb3cdf05f09eecda921646e15135"}, + {file = "msgpack-1.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:d626fcacecaf3a5bfcd57e35709ce59a56a39cd4a3cd201d1b43c00d20bcbcda"}, + {file = "msgpack-1.2.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1095b1ebcc8c9d3e70c51240b7160d8e9da66044e0a45c3ef8403d3a9ef0c5ad"}, + {file = "msgpack-1.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a4b47ec7422bef149223786a87df80067ac65bfc7c0565e5cf1dfbdc40acee50"}, + {file = "msgpack-1.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c75cfda0d3ccb1f6abc20e0608ad6b226e11f317b5ac297721504d2509e2bd9"}, + {file = "msgpack-1.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a45ceb5f199d5e388f28b550ba881096a3740d4bda10e661d77f59d9a2e8187a"}, + {file = "msgpack-1.2.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ca62b31a873c70b73f367bb2c1efec1d143016642c5191405549578184ff6ff"}, + {file = "msgpack-1.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f641971d3eec89ffca57fa36d151ad1cf1b6a214b47faca16cb165c7d45ef8be"}, + {file = "msgpack-1.2.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b9eff629190a4b118549b654df32a56ed535351819f690d0c935d185500cc90b"}, + {file = "msgpack-1.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4e22c6571ab6ac9036da87d8a6fddb4baf86981844b480c0c60a4a4be417ee48"}, + {file = "msgpack-1.2.4-cp311-cp311-win32.whl", hash = "sha256:5d7d4d2a5fc6a604f20c546ee1e8327bed1e9329be2417cef9af1f799f3e55e0"}, + {file = "msgpack-1.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9db832747bd05834530189e564a3fe31e2617547dc1f3a005f835eafc2b8e048"}, + {file = "msgpack-1.2.4-cp311-cp311-win_arm64.whl", hash = "sha256:deb1730c4c0c1a8c975be2f43efea6ee4834d35031a39075f78917d0ba8b3c04"}, + {file = "msgpack-1.2.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:90cba4996b18ec1201fcaabd01ec48ff4b9755416bcd52105ded2f89fe145c49"}, + {file = "msgpack-1.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:112933726919f4239646cca9572e2c2fa5626358c670da8440aac09403100a48"}, + {file = "msgpack-1.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ca1e78106675f2aed07dc37b842ce19be70010ab22c5c5c04688f0840d48539"}, + {file = "msgpack-1.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4d5bbdbea8f5669b3a734d6c226772804fb120848f99cbe63a0ddee63b049c9"}, + {file = "msgpack-1.2.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d9ff7f3aaffd84010f1c501b05406a497584f5cab2f732eb9d89bf960e12f6f"}, + {file = "msgpack-1.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa10dc04c03408f31948ac1896a942141fd8d7a954b3612edac79f19663e4076"}, + {file = "msgpack-1.2.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:472a25463678f2795143a7cb42e88a1b59cc2375e9881917e69a9323bde42114"}, + {file = "msgpack-1.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cbb8a677da3e845713925e9a203b8a120b1e9da9989836890ba4a4a187902701"}, + {file = "msgpack-1.2.4-cp312-cp312-win32.whl", hash = "sha256:2b0b7dff5beddc10f16ecea6b28c6b98014d1135d7015bd83adff5037f35848d"}, + {file = "msgpack-1.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:6e38b123681df22373d545520f54bbeddc4706d9235a1161061f12bd6648497d"}, + {file = "msgpack-1.2.4-cp312-cp312-win_arm64.whl", hash = "sha256:6795c588b210b2f13d62c8fcc23bf779b509b839ec5c197a7ab4f5191459e126"}, + {file = "msgpack-1.2.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fb811ac3d463a75013b234d2aae56b9f0bdb0105caf4dd2ee65305b6a8a6078d"}, + {file = "msgpack-1.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:36258fd0909e1bf26e94604439108846432abe4ffa9a23c9dc2f6f1c60f94b60"}, + {file = "msgpack-1.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9c5ba0f053a7e98f013f083c9d1b2dffb26cad8c897c533d8286e44ed24c669"}, + {file = "msgpack-1.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fabd415c41e62006c6641fcdf5b3e866f581c4e11c58328c0d0f69584b9577d"}, + {file = "msgpack-1.2.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7433d340de45003db3ed909a642c9469b598d175530f7a707cbaba64287d27ce"}, + {file = "msgpack-1.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccd6e4af5347eb9828147320449bcd30e3321f6397d4583ff16047485dd8be3d"}, + {file = "msgpack-1.2.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:23d1459a1d0b4c9f6629df072d592eb74424b3ed3656b69af507bc9023cc027f"}, + {file = "msgpack-1.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e73761fe9fc55a4e9d2df8e8f59b0979231af4dbf80b8bf90cd64c74173d779"}, + {file = "msgpack-1.2.4-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:d088d1f552e129859db830692268747aba0a1ee3e6e0fe6344dbb3aab1df9f6c"}, + {file = "msgpack-1.2.4-cp313-cp313-win32.whl", hash = "sha256:892d4f17d7962bfd2c2928b0a47542bf2948f074a7c04721b73d4138b70481e4"}, + {file = "msgpack-1.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:318d4e634d2b320be10decd9968ad1ae0be1d80cba23b5528c37b429eeb0e3e0"}, + {file = "msgpack-1.2.4-cp313-cp313-win_arm64.whl", hash = "sha256:bec3e34749b90cfcf116149aad11295c720ff4a74a7d0a35abdb968a396c1524"}, + {file = "msgpack-1.2.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:895e9369185df4a874768849405c30cba7990caceb3fcb1692ebe62bb050f55e"}, + {file = "msgpack-1.2.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c97f5f17407e1761f23f1944be33cbf1d5ce67017db32544dcd5e16e0b1b4a3b"}, + {file = "msgpack-1.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2444a4e04f924630d059ae6eae8f91fdfcd28ed9fd17b21bacf4e74550a091f7"}, + {file = "msgpack-1.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d65b791ce4283e02b6267742ad540899155e1b32887b9cbdda47f38f5a076b5"}, + {file = "msgpack-1.2.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5e4c67dbac05dce90a3fa1d845137617383c415e6b319062baac9734540706ae"}, + {file = "msgpack-1.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:781e856db4f2cb398e4992e2a74305295d3ebf694eec26fa0f4bc766eb9fa0a0"}, + {file = "msgpack-1.2.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cfc1cc237792f9a5664002b66acb961027d4184528ec9d10e0e42330f7972383"}, + {file = "msgpack-1.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f60de6ae13d4d2a12a483f15c5e47cb6fa2731aa32d92fb31178e2017d6574a8"}, + {file = "msgpack-1.2.4-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:06a0657a4110d563255006b7cb7147432f67827144e429c2cdb1c967548d1817"}, + {file = "msgpack-1.2.4-cp314-cp314-win32.whl", hash = "sha256:727a65b35db9f5ef516fc0229c4b05cab983092b1ae274a8a769b2d517fb0499"}, + {file = "msgpack-1.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:80f5d4fda1ba373d95933f98e6c1d18247feffac839fda3930bdd85e6561bb5f"}, + {file = "msgpack-1.2.4-cp314-cp314-win_arm64.whl", hash = "sha256:e5f67f0286f2ba2c6dbb4998404ed880112ca204ceda729cf239879cd89d3acf"}, + {file = "msgpack-1.2.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:45506bfb902cde24a0d33fb0581f58497ddb2682686dd1f6aa07e7b769df7ca4"}, + {file = "msgpack-1.2.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dc9dc681958e69f46b9f37def3e7fd6cc5dcfaa5943fb00bfe9c223a1a1052f6"}, + {file = "msgpack-1.2.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bef691e2a95119cc6bf3ded29325dbd89c4b6f8ff233070431948ed70902421"}, + {file = "msgpack-1.2.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e6106d2d085af42b2de72efeb38ed255241a24b32295ca889707c1d679c442"}, + {file = "msgpack-1.2.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:996dfc4774c240c8e6adf1166ae102141cfbed33c8856fde821bd6c0bd35ee41"}, + {file = "msgpack-1.2.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:efa1357fb0c7201e833de28638be371ee127476f7ccff9e7a94211b0afc33ddf"}, + {file = "msgpack-1.2.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b9270b63409e70c8c6deb5faedc81fc27c6bd0196b38084a8773f68286abf034"}, + {file = "msgpack-1.2.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2b597bb01aff61d65e305c2cf2589c93c86609929d42d575bfc359f3c54ea8e1"}, + {file = "msgpack-1.2.4-cp314-cp314t-win32.whl", hash = "sha256:ad5bc540cbc34db236f8535721a9a872d12a9d88d1322470f1fc7c5e9dadfdb2"}, + {file = "msgpack-1.2.4-cp314-cp314t-win_amd64.whl", hash = "sha256:287e52c2ebaa94f8eca045628374859f51584e77bc07086e6835a9bb2204c774"}, + {file = "msgpack-1.2.4-cp314-cp314t-win_arm64.whl", hash = "sha256:f3f19fe5decd348b35b003ae5f32d3fa90877413a0183da676f748a3a7ab9270"}, + {file = "msgpack-1.2.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:765584b93ed6f0b561f0c56e9735f24945c0682f857c9b3ee3b4caad9c32f66a"}, + {file = "msgpack-1.2.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:0221748c30ace75fde0dec8c470abe65fb5374cd4f37adb9db6aca7a3c362c0a"}, + {file = "msgpack-1.2.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5917972ce1bf5f85f761031e00dab6fe0ad9f880cb3d2d43ed74b330c850400"}, + {file = "msgpack-1.2.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7ae8cf6012372cecb43016132d1b87ccc2ebb8ae5ab5078687399e82423386a"}, + {file = "msgpack-1.2.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e78c66cf97ac5675037f705ca433ad98ac9177f9523e7fdbb34e486ae29044c8"}, + {file = "msgpack-1.2.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:59887281c3c163a7afff7dffaaad2f78cd2b513b7e1a7843f1ee8e7cd6a94231"}, + {file = "msgpack-1.2.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:9f46e70c8d22e96856967ab9d80c51dcf5bc63133e4da2d66fab64c0d4119b82"}, + {file = "msgpack-1.2.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:0a1fd6c31952155b7cd01061288c70303e6554ae855244dfb38a897fd94f8749"}, + {file = "msgpack-1.2.4-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:b1f08ec826417e3c8dc3edadb50f38debba033af6cb849229f0eac72f80f2342"}, + {file = "msgpack-1.2.4-cp315-cp315-win32.whl", hash = "sha256:e8ba23c838739fe929e868095171e71f2f38d6ce0fcf5840b576fb82648e58e6"}, + {file = "msgpack-1.2.4-cp315-cp315-win_amd64.whl", hash = "sha256:67310606cf2528a6dc3ece1298dcd5cc6c57add148b16d22c5111138aa84820c"}, + {file = "msgpack-1.2.4-cp315-cp315-win_arm64.whl", hash = "sha256:ce76e7a7c11459157887a8dadf07c107ff74eca98cf914f0de6ee7ab4b47104d"}, + {file = "msgpack-1.2.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:fb9ddbf23b8c6e89752af399fcf4daed60055e239cda44c6acfaa76d19331e8c"}, + {file = "msgpack-1.2.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e283cf563166c21762131b88e3870ee2ec57757b08a8736929757b5a67541061"}, + {file = "msgpack-1.2.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e91bf8325f492e8eb3c3aa6de06b20593c225336bb52b585c913d7c1da025b"}, + {file = "msgpack-1.2.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfa86432bf329d6b976f5a00ad4c9064fd51736e4967a52526750236345cc2da"}, + {file = "msgpack-1.2.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:42e0a7dc26df739149478585d4264338b4d330f24b3e3b57d5aa89e48866e8ab"}, + {file = "msgpack-1.2.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:8e680367090a58c966052d61418cc943111863538fd82471d61fca5271d911e1"}, + {file = "msgpack-1.2.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:a6c9926f8871b261622fe6b599db72c69be5bca8fdc0976886f9a531a301dee9"}, + {file = "msgpack-1.2.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:c50bac325dbf1a9c80e10da6dbd1100d7082f6b12e153084501e18e86cd3a037"}, + {file = "msgpack-1.2.4-cp315-cp315t-win32.whl", hash = "sha256:34051de19ae532ba47fcab5b0abd08c25c7a64ff763ad819f18764d45f3c2c81"}, + {file = "msgpack-1.2.4-cp315-cp315t-win_amd64.whl", hash = "sha256:6ad2f0808099d4281208ff2c756efa958a55a7c8eb21fe1c7052b95c584f02f1"}, + {file = "msgpack-1.2.4-cp315-cp315t-win_arm64.whl", hash = "sha256:e38d854f1c60e94f46d998765201126338bde3fe1b029be0d849094bcbef72c2"}, + {file = "msgpack-1.2.4.tar.gz", hash = "sha256:475c44e12f2a5de766a8448783f149c9b33f3f49b88953c6cd739a77d93c732c"}, ] [[package]] name = "mypy" -version = "2.1.0" +version = "2.4.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "mypy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:11a6beb180257a805961aea9ec591bbd0bd17f1e18d35b8456d57aee5bedfedc"}, - {file = "mypy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ef78c1d306bbf9a8a12f526c44902c9c28dffd6c52c52bf6a72641ce18d3849"}, - {file = "mypy-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c209a90853081ff01d01ee895cafe10f7db1474e0d95beaeef0f6c1db9119bbd"}, - {file = "mypy-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47cebf61abde7c088a4e27718a8b13a81655686b2e9c251f5c0915a802248166"}, - {file = "mypy-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d57a90ae5e872138a425ec328edbc9b235d1934c4377881a33ec05b341acc9a8"}, - {file = "mypy-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:aea7f7a8a55b459c34275fc468ada6ca7c173a5e43a68f5dbe588a563d8a06b8"}, - {file = "mypy-2.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c989640253f0d76843e9c6c1bbf4bd48c5e85ada61bde4beb37cb3eca035685e"}, - {file = "mypy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a683016b16fe2f572dc04c72be7ee0504ac1605a265d0200f5cea695fb788f41"}, - {file = "mypy-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a293c534adb55271fef24a26da04b855540a8c13cc07bc5917b9fd2c394f2ca"}, - {file = "mypy-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7406f4d048e71e576f5356d317e5b0a9e666dfd966bd99f9d14ca06e1a341538"}, - {file = "mypy-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0210d626fc8b31ccc90233754c7bc90e1f43205e85d96387f7db1285b55c398"}, - {file = "mypy-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3712c20deed54e814eaaa825603bada8ea1c390670a397c95b98405347acc563"}, - {file = "mypy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fcaa0e479066e31f7cceb6a3bea39cb22b2ff51a6b2f24f193d19179ba17c389"}, - {file = "mypy-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:0b1a5260c95aa443083f9ed3592662941951bca3d4ca224a5dc517c38b7cf666"}, - {file = "mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af"}, - {file = "mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6"}, - {file = "mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211"}, - {file = "mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b"}, - {file = "mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22"}, - {file = "mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b"}, - {file = "mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8"}, - {file = "mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5"}, - {file = "mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e"}, - {file = "mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e"}, - {file = "mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285"}, - {file = "mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5"}, - {file = "mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65"}, - {file = "mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d"}, - {file = "mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2"}, - {file = "mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f"}, - {file = "mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4"}, - {file = "mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef"}, - {file = "mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135"}, - {file = "mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21"}, - {file = "mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57"}, - {file = "mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e"}, - {file = "mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780"}, - {file = "mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd"}, - {file = "mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08"}, - {file = "mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081"}, - {file = "mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7"}, - {file = "mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6"}, - {file = "mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289"}, - {file = "mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633"}, -] - -[package.dependencies] -ast-serialize = ">=0.3.0,<1.0.0" -librt = {version = ">=0.11.0", markers = "platform_python_implementation != \"PyPy\""} + {file = "mypy-2.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2b4e0aa029ad0c1d42cc494daf86d1e0efd15dab4db7bd096ece29e8a9689c6a"}, + {file = "mypy-2.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c535efcb91f7e66f41b9bd2b5dbd78900a7609ce88f4c020d668e4c82d082b0b"}, + {file = "mypy-2.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0724cf95fabc87ca4a2d5b7f276b43f51afce8a3804bde704490b008e377e50b"}, + {file = "mypy-2.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0f0a8b8f5dee799195a3f2670a5b2617ac7193d3563f52dbef42ad6770c6a9db"}, + {file = "mypy-2.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:d4c343b2a720741a016d720b255e75581a50e2bede61d53250484e18562a1d64"}, + {file = "mypy-2.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:8e437811ebd26d3e6258efb562a711b385c139be597dffed5e735c801d15c77f"}, + {file = "mypy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:79a1e79199e980fb41f219cb3d4746d25c64b0b623840d3ed57df1cdc13a6502"}, + {file = "mypy-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e18d0da3898b3bcd16192f15a206e7b8b8dd0f4a0b3e51aa5bd5c2afe749e2c"}, + {file = "mypy-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b69a15fc2e75858d6b15e91ed3a0dcff1a533265183b64734138791fe0f49aa5"}, + {file = "mypy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:70971cc0bcb1d3f03d2eb68fa430a6481ccc03558dc001d9534d7e5fa274c392"}, + {file = "mypy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:6f7225951a277dd0d6add900731d487174b7d24ac133c09d670ed63b4d8cb90d"}, + {file = "mypy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:be4c03f33c55500191df2ed4a9b5e7edbdd1fff067fe361a5a9b1b5d3262da6e"}, + {file = "mypy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d63737426a048a27705a4eedb828669f4bc0873612a1b46b0b9fe892b2ebaaaf"}, + {file = "mypy-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7344869cad05f073b8cae2826774fa0161e3eb2f5c685b76813574008d77d750"}, + {file = "mypy-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a20ff6cd2af7a670c64f089bfa5fbcebe8ff3bf896c5e6ac7c9862c7960e4494"}, + {file = "mypy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9ed1195502cca32be5ef756b1693477f4991db2f2e34eaa90cc4ad6bea4f9531"}, + {file = "mypy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:4c194c5d878130fca1ae662dfb1feb6274afbab0b40032c80147377e0ea803d5"}, + {file = "mypy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:71eda06e5bae51976ce869fda429159c9cec678bd02f25bb197f0265c8f991be"}, + {file = "mypy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f1613bfe6726169a5680ec3dad5fe1f6d96285c12aded51ad432eadedd39d45"}, + {file = "mypy-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:680ba5d9f0e3fe0c746b3d14df15f2a718e4bc308a1145616a1ea691ce7c354b"}, + {file = "mypy-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb70621535c22fd4b1a0450d37ed1d6c5eb39656a1cebfb189084c12de54ba7"}, + {file = "mypy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4af5ad558033bf71a44a1be149ed61633522824f84c1b6bc2445d3fe2d2a5296"}, + {file = "mypy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:acdd82cb3a2eb10720e87706a128d75785d005a8c47e36a4973221524f41fc56"}, + {file = "mypy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:133886ef1a7a8cf673df55d865e41580c6c46aa9b8e31e1325dfe315c038f745"}, + {file = "mypy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c45485061ba08f700acc12cfc8b177e6997e6bc353605babfc0abb081e079456"}, + {file = "mypy-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727890a32f1858c09549eb55a8b234fe990d460450b28f03e8213ef925f1247c"}, + {file = "mypy-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:beb254fec368c4e7f7f82a5f310c6470b893ec88a90b66426da971c6826b0453"}, + {file = "mypy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ec416d5d2ae689e681f5727e328723f18d8b5dc8c903a146b7851dc5727aad8a"}, + {file = "mypy-2.4.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:0e95ad0bec03ced9a73b42514208abd24c789e67f0e1eba476e276dd7a2dea3a"}, + {file = "mypy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:51f742d4154824837ca6e92aa81f00eec222700a3c337bd258e5562a3880d75a"}, + {file = "mypy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:f44dd4c5ab22df21206dd926c98381ebdbf5bfd696df37dd38ded2d23143fcb6"}, + {file = "mypy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2875280687e64740e6ddcef43bd4c5852ad45ff34549740f6aea3f0373870b93"}, + {file = "mypy-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea0714d2bccaf6d010c970e1d3ba2c9c2add79d2030aad2ef03167d788948276"}, + {file = "mypy-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c6ff0e7ea546943001a91639c9f0c64b60d239268fa5d29e4a6c87f82c2373"}, + {file = "mypy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a5b5da107dc2c93fb0a22f7a45dbaea5dc131afc7056b5468d78e819f9a138e6"}, + {file = "mypy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c3a4d0c47053cc90035dd3a2c321112b593ad42935d0cbfd747288e1564fa8ca"}, + {file = "mypy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0c5bf267645351887b72903fd2f5fbddcf85234e0e6c4ad94e2fcfb4f0c3f971"}, + {file = "mypy-2.4.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:db599d03e7d60d15f2e0a11665c18bc52a88699fcab990fdf8cecd5be8ac801c"}, + {file = "mypy-2.4.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6c7210e40dd423af7d65cbe428b5906d11dc60659337bdae4e5fdf02ce32ed7"}, + {file = "mypy-2.4.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2d4a51db92059eecc2c33b2a5194b28585ebfd917292c015fe87a77dbff6cd2"}, + {file = "mypy-2.4.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:01b7718552d017f88d1151f3cc9d6556d778a90bd58060a2c3d87d070f9099fb"}, + {file = "mypy-2.4.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:3467b1d13000812ab85dc2a3834b9379ec77798ca1bcabe434e23fb21fead692"}, + {file = "mypy-2.4.1-cp315-cp315-win_amd64.whl", hash = "sha256:080cf64a60b90a73435542feecf4171904cc5710658165b40c6869115d4aa100"}, + {file = "mypy-2.4.1-cp315-cp315-win_arm64.whl", hash = "sha256:f9118a3f50de264b614861bb11814833a742138ac3c8e2e49a860b6977fb2704"}, + {file = "mypy-2.4.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9d7dafd56179d296651df9e1844b91c9517c379398f017a2a435d077a28943c1"}, + {file = "mypy-2.4.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d923cec43316f0e6736b8298fd00abeaaf348a091c20e7296b3aebceecb63eca"}, + {file = "mypy-2.4.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6e081b466e88d4343adffe11aff0f02df34b5b9070b4b700053a0b7294c0be1"}, + {file = "mypy-2.4.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:70196470ac300f444a302a738541101893c2c57804078b33690286a7899149e9"}, + {file = "mypy-2.4.1-cp315-cp315t-win_amd64.whl", hash = "sha256:de95648e0a6d46fa9929b956ed2c87c5dc9933d4d59945e6390e9d753c74e7c1"}, + {file = "mypy-2.4.1-cp315-cp315t-win_arm64.whl", hash = "sha256:ef8d5be90032261197ac388d945d8ff4f476c95880798a1911760af64ddfe71f"}, + {file = "mypy-2.4.1-py3-none-any.whl", hash = "sha256:1784648d6ec8c2b5465e3f26241b845a0b3cb4e56c993c64abe16989e7698999"}, + {file = "mypy-2.4.1.tar.gz", hash = "sha256:63b590965691e540a335fa74dadf31a7069142f6b72e234689bbd244edf55623"}, +] + +[package.dependencies] +ast-serialize = ">=0.12.1,<1.0.0" +librt = {version = ">=0.16.0", markers = "platform_python_implementation != \"PyPy\""} mypy_extensions = ">=1.0.0" pathspec = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} @@ -1897,14 +2337,14 @@ test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>= [[package]] name = "nbconvert" -version = "7.17.1" +version = "7.17.2" description = "Convert Jupyter Notebooks (.ipynb files) to other formats." optional = false python-versions = ">=3.9" groups = ["docs"] files = [ - {file = "nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8"}, - {file = "nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2"}, + {file = "nbconvert-7.17.2-py3-none-any.whl", hash = "sha256:6a143c11c00bf831604da4b1be4347d36ea52367057eb920135d69bc2862fed2"}, + {file = "nbconvert-7.17.2.tar.gz", hash = "sha256:344a218e8595117ce37396ed025d994f146d076226ebac59255a2fa085ab24d8"}, ] [package.dependencies] @@ -1934,14 +2374,14 @@ webpdf = ["playwright"] [[package]] name = "nbformat" -version = "5.10.4" +version = "5.11.1" description = "The Jupyter Notebook format" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["docs"] files = [ - {file = "nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b"}, - {file = "nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a"}, + {file = "nbformat-5.11.1-py3-none-any.whl", hash = "sha256:cc6698fa75f4fab8755ead786317815f13a6fee3b53311c0abb1a8b51d52f7ec"}, + {file = "nbformat-5.11.1.tar.gz", hash = "sha256:32d4521c68c6e7d5b29c76defaeed9f42ea733142b9b19f88277ce10390b9c4d"}, ] [package.dependencies] @@ -1951,8 +2391,8 @@ jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" traitlets = ">=5.1" [package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["pep440", "pre-commit", "pytest", "testpath"] +docs = ["intersphinx-registry", "myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["packaging", "pre-commit", "pytest", "testpath"] [[package]] name = "nbsphinx" @@ -1988,41 +2428,47 @@ files = [ [[package]] name = "nodeenv" -version = "1.10.0" +version = "1.11.0" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["dev"] files = [ - {file = "nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827"}, - {file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"}, + {file = "nodeenv-1.11.0-py2.py3-none-any.whl", hash = "sha256:edaa16e6c14d7cf395d75d4bbd5a26390f4dc06501a33b4e76282b02cc688a25"}, + {file = "nodeenv-1.11.0.tar.gz", hash = "sha256:3ce8fe5b71d16e8af7039ca65257354100bc772965d6bc549070649e53b1b146"}, ] +[package.extras] +certifi = ["certifi"] + [[package]] name = "owlrl" -version = "7.1.4" +version = "7.6.2" description = "A simple implementation of the OWL2 RL Profile, as well as a basic RDFS inference, on top of RDFLib. Based mechanical forward chaining." optional = false -python-versions = "<4.0,>=3.9" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "owlrl-7.1.4-py3-none-any.whl", hash = "sha256:e78b46020169783345636da93a467d318f18700c483184dd15e885850cf64775"}, - {file = "owlrl-7.1.4.tar.gz", hash = "sha256:60bd4067e346b9111f0a2924565afe97ac6595b98b2bbe953928b5113971daf7"}, + {file = "owlrl-7.6.2-py3-none-any.whl", hash = "sha256:83347bf7f133979e87b2b18695d51d25510b99cec3f6919b5df05d4fbf058ae0"}, + {file = "owlrl-7.6.2.tar.gz", hash = "sha256:c743f35c2d908396e77823852bb1ebbce88340cd49961493983bec42c93283a8"}, ] [package.dependencies] -rdflib = ">=7.1.4" +rdflib = ">=7.6.0" + +[package.extras] +oxigraph = ["pyoxigraph (>=0.5.6)"] [[package]] name = "packaging" -version = "26.2" +version = "26.3" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "dev", "docs", "test"] files = [ - {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, - {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, + {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, + {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, ] [[package]] @@ -2099,46 +2545,77 @@ files = [ ] [package.extras] -docs = ["Sphinx (>=4.1.2,<5.0.0)", "furo (>=2021.8.17-beta.43,<2022.0.0)", "myst-parser (>=0.15.1,<0.16.0)", "sphinx-autobuild (>=2021.3.14,<2022.0.0)", "sphinx-copybutton (>=0.4.0,<0.5.0)"] +docs = ["Sphinx (>=4.1.2,<5.0.0)", "furo (>=2021.8.17b43,<2022.0.0)", "myst-parser (>=0.15.1,<0.16.0)", "sphinx-autobuild (>=2021.3.14,<2022.0.0)", "sphinx-copybutton (>=0.4.0,<0.5.0)"] [[package]] name = "platformdirs" -version = "4.10.0" +version = "4.12.4" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" groups = ["main", "dev", "docs"] +markers = "python_version == \"3.10\"" files = [ - {file = "platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a"}, - {file = "platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7"}, + {file = "platformdirs-4.12.4-py3-none-any.whl", hash = "sha256:78bfb9db2a8471ed7eebe3c3c932da413911042994e699b384fbb4493fa872d7"}, + {file = "platformdirs-4.12.4.tar.gz", hash = "sha256:63743c02414e755de4e31b8f68125c1407495b86c5a006e203c01ff8b9924250"}, +] + +[[package]] +name = "platformdirs" +version = "4.14.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.11" +groups = ["main", "dev", "docs"] +markers = "python_version >= \"3.11\"" +files = [ + {file = "platformdirs-4.14.0-py3-none-any.whl", hash = "sha256:1dadfc59a8a5d7565b3d9270ae8fa71c065595352c4277b41c11ee6c222c4b91"}, + {file = "platformdirs-4.14.0.tar.gz", hash = "sha256:83f009c1e8c5fc36869b5171755b291887bd2cade7965233cdfabd23d754720b"}, ] [[package]] name = "pluggy" -version = "1.6.0" +version = "1.7.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["test"] files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, + {file = "pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec"}, + {file = "pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8"}, ] -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] - [[package]] name = "pre-commit" -version = "4.6.0" +version = "4.6.2" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.10" groups = ["dev"] +markers = "python_version == \"3.10\"" files = [ - {file = "pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b"}, - {file = "pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9"}, + {file = "pre_commit-4.6.2-py2.py3-none-any.whl", hash = "sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e"}, + {file = "pre_commit-4.6.2.tar.gz", hash = "sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pre-commit" +version = "4.7.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.11" +groups = ["dev"] +markers = "python_version >= \"3.11\"" +files = [ + {file = "pre_commit-4.7.0-py2.py3-none-any.whl", hash = "sha256:2e229038ad3656081b70c27913157e4b636ef306f643f024a8a8e3f0f7b2f877"}, + {file = "pre_commit-4.7.0.tar.gz", hash = "sha256:1f3deee12b914c8fa184bd2b5953391c2e4f36763e403d4a23dc9983fa7c6e86"}, ] [package.dependencies] @@ -2168,18 +2645,18 @@ tests = ["pytest (>=9)", "pytest-cov", "pytest-lazy-fixtures"] [[package]] name = "prompt-toolkit" -version = "3.0.52" +version = "3.0.53" description = "Library for building powerful interactive command lines in Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955"}, - {file = "prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855"}, + {file = "prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2"}, + {file = "prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6"}, ] [package.dependencies] -wcwidth = "*" +wcwidth = ">=0.1.4" [[package]] name = "psutil" @@ -2231,14 +2708,14 @@ files = [ [[package]] name = "pure-eval" -version = "0.2.3" +version = "0.2.4" description = "Safely evaluate AST nodes without side effects" optional = false python-versions = "*" groups = ["dev"] files = [ - {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, - {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, + {file = "pure_eval-0.2.4-py3-none-any.whl", hash = "sha256:96cae060a313cfaad51bb761278bfb0e62dc0248d9315a81173752dc546cd37a"}, + {file = "pure_eval-0.2.4.tar.gz", hash = "sha256:260c2774686e651b79f8b8e7fc9d80b3599ea6a66334b47d5f4abb69fc2c0ea1"}, ] [package.extras] @@ -2246,32 +2723,60 @@ tests = ["pytest"] [[package]] name = "pycparser" -version = "3.0" +version = "3.11" description = "C parser in Python" optional = false python-versions = ">=3.10" groups = ["dev", "docs"] -markers = "implementation_name == \"pypy\"" +markers = "python_version == \"3.10\" and implementation_name == \"pypy\"" files = [ - {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, - {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, + {file = "pycparser-3.11-py3-none-any.whl", hash = "sha256:51d5a8ba2be0bbe440b99d2112604c95bbbc3c2748a64260186c541e1729cd80"}, + {file = "pycparser-3.11.tar.gz", hash = "sha256:d875f09c3507d00e1aba0eecc6dcadc1352f30fff09dc6bff2f1c2935e97c2bc"}, +] + +[[package]] +name = "pycparser" +version = "3.12" +description = "C parser in Python" +optional = false +python-versions = ">=3.11" +groups = ["dev", "docs"] +markers = "python_version >= \"3.11\" and implementation_name == \"pypy\"" +files = [ + {file = "pycparser-3.12-py3-none-any.whl", hash = "sha256:a076f3f489b39b43ed13563a1dafe42447445bec2e294154ec5c57fcdda4c85e"}, + {file = "pycparser-3.12.tar.gz", hash = "sha256:f03387877eab82ce01bbdfd0ba7252ee55a90b1c708d5129e2ce915475cf1db6"}, ] [[package]] name = "pygments" -version = "2.20.0" +version = "2.21.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.9" groups = ["main", "dev", "docs", "test"] files = [ - {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, - {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, + {file = "pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9"}, + {file = "pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c"}, ] [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pygments-styles" +version = "0.3.0" +description = "A curated collection of Pygments styles based on VS Code themes." +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "pygments_styles-0.3.0-py3-none-any.whl", hash = "sha256:c6c45e9939eb7590345bc9084113bac46c45f12b009d13422be02e80e84a034c"}, + {file = "pygments_styles-0.3.0.tar.gz", hash = "sha256:67746b8fc6ff72c1179ca4d9a8bc89c7f54c196c2ff9d087f07392cd6fde3ecf"}, +] + +[package.dependencies] +pygments = "*" + [[package]] name = "pylint" version = "3.3.9" @@ -2289,8 +2794,8 @@ astroid = ">=3.3.8,<=3.4.0.dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, - {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, {version = ">=0.3.6", markers = "python_version == \"3.11\""}, + {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, ] isort = ">=4.2.5,<5.13 || >5.13,<7" mccabe = ">=0.6,<0.8" @@ -2302,16 +2807,33 @@ tomlkit = ">=0.10.1" spelling = ["pyenchant (>=3.2,<4.0)"] testutils = ["gitpython (>3)"] +[[package]] +name = "pypandoc-binary" +version = "1.17" +description = "Thin wrapper for pandoc." +optional = false +python-versions = ">=3.7" +groups = ["docs"] +files = [ + {file = "pypandoc_binary-1.17-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:734726dc618ef276343e272e1a6b4567e59c2ef9ef41d5533042deac3b0531f1"}, + {file = "pypandoc_binary-1.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fcfd28f347ed998dda28823fc6bc24f9310e7fdf3ddceaf925bf0563a100ab5b"}, + {file = "pypandoc_binary-1.17-py3-none-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b620b21c9374e3e48aabd518492bf0776b148442ee28816f6aaf52da3d4387"}, + {file = "pypandoc_binary-1.17-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ada156cb980cd54fd6534231788e668c00dbb591cbd24f0be0bd86812eb8788"}, + {file = "pypandoc_binary-1.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2f439dcd211183bb3460253ca4511101df6e1acf4a01f45f5617e1fa2ad24279"}, + {file = "pypandoc_binary-1.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f6e6d3e4cfafbe23189a08db3d41f8def260bacd6e7e382bceadab7ba1f17da6"}, + {file = "pypandoc_binary-1.17-py3-none-win_amd64.whl", hash = "sha256:76fae066cd2d7e78fb97f0ec8e9e36f437b07187b689b0b415ca18216f8f898a"}, +] + [[package]] name = "pyparsing" -version = "3.3.2" +version = "3.3.3" description = "pyparsing - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"}, - {file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"}, + {file = "pyparsing-3.3.3-py3-none-any.whl", hash = "sha256:ece8c00a69cf01b45d0b1dedabb469c90d8caf996d4fda40f147627a122849a4"}, + {file = "pyparsing-3.3.3.tar.gz", hash = "sha256:928ae7e20211f3b6f3915a72f06a0cfd29ab9d24279dd6346b6b1a7146397d36"}, ] [package.extras] @@ -2319,56 +2841,57 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pyshacl" -version = "0.31.0" +version = "0.40.2" description = "Python SHACL Validator" optional = false -python-versions = "<4,>=3.9" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pyshacl-0.31.0-py3-none-any.whl", hash = "sha256:5cae2184401d956b67deebb00e3c78ab7052784741a730e52e309e33c8a0b9a5"}, - {file = "pyshacl-0.31.0.tar.gz", hash = "sha256:327950875a5bb0d1a15c246a8a272b2dbf6bc9b96e28cfa8fdbfa4d73aadc0ba"}, + {file = "pyshacl-0.40.2-py3-none-any.whl", hash = "sha256:edf0a7c15074902a1a420e8cfb89a764a9eed990b6ea15b8c30c40f2220eef61"}, + {file = "pyshacl-0.40.2.tar.gz", hash = "sha256:3b089fcfe82dccca2a9d560d88665ca0345f828b9cf21de554e611a55c2c7277"}, ] [package.dependencies] importlib-metadata = {version = ">6", markers = "python_version < \"3.12\""} -owlrl = ">=7.1.2,<8" +owlrl = ">=7.6.2,<8" packaging = ">=21.3" prettytable = [ {version = ">=3.5.0", markers = "python_version < \"3.12\""}, {version = ">=3.7.0", markers = "python_version >= \"3.12\""}, ] -rdflib = {version = ">=7.1.1,<7.1.2 || >7.1.2,<8.0", extras = ["html"]} +rdflib = {version = ">=7.3.0,<8.0", extras = ["html"]} [package.extras] dev-coverage = ["coverage (>6.1,!=6.1.1,<7)", "platformdirs", "pytest-cov (>=2.8.1,<3)"] dev-lint = ["platformdirs", "ruff (>=0.9.3,<0.10)"] dev-type-checking = ["mypy (>=1.13.0)", "platformdirs", "types-setuptools"] http = ["sanic (>=22.12,<23)", "sanic-cors (==2.2.0)", "sanic-ext (>=23.3,<23.6)"] -js = ["pyduktape2 (>=0.4.6,<1)"] +js = ["pyduktape2 (>=0.4.6,<1) ; python_version <= \"3.13\"", "pyduktape2 (>=0.5.0,<1) ; python_version >= \"3.13\""] +oxigraph = ["pyoxigraph (>=0.5.6)"] [[package]] name = "pytest" -version = "9.1.1" +version = "9.2.0" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.10" groups = ["test"] files = [ - {file = "pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"}, - {file = "pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313"}, + {file = "pytest-9.2.0-py3-none-any.whl", hash = "sha256:07acdcf88ec174f907dc01af70bf52d49b014bbab03faeeade31c96b18c2e588"}, + {file = "pytest-9.2.0.tar.gz", hash = "sha256:97c4f3b1f3c7414aa36465e09b8220f1d03627993bd51b1282e193753c80557b"}, ] [package.dependencies] colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} -iniconfig = ">=1.0.1" -packaging = ">=22" -pluggy = ">=1.5,<2" -pygments = ">=2.7.2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=2" +packaging = ">=24" +pluggy = ">=1.6,<2" +pygments = ">=2.15" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} [package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "asynctest ; python_version < \"3.11\"", "attrs (>=23.1)", "coverage (>=7.5)", "hypothesis (>=6.75)", "mock", "numpy (>=1.26)", "pexpect (>=4.9)", "pytest-xdist (>=3.5)", "pyyaml", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" @@ -2412,38 +2935,34 @@ testing = ["filelock"] [[package]] name = "python-dateutil" -version = "2.9.0.post0" +version = "3.0.0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["dev", "docs"] files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, + {file = "python_dateutil-3.0.0-py2.py3-none-any.whl", hash = "sha256:3c79630202e4152c6add678446984236efe4f57e84d8e892aab0637bab82e9fd"}, + {file = "python_dateutil-3.0.0.tar.gz", hash = "sha256:7ebcb3c29a92f3ceafe79a7a48a5c45677e9e004bbeec5742ac0bbb00560b7a2"}, ] [package.dependencies] -six = ">=1.5" +six = ">=1.6" +tzdata = "*" [[package]] name = "python-discovery" -version = "1.4.2" +version = "1.6.7" description = "Python interpreter discovery" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "python_discovery-1.4.2-py3-none-any.whl", hash = "sha256:475803f53b7b2ed6e490e27373f9d8340f7d2eebf9acdaf645d7d714c97bb500"}, - {file = "python_discovery-1.4.2.tar.gz", hash = "sha256:8f3746c4b4968d22afbb97d36e1a0e5b66e6c0f297290f2e95f05b9b8bf18690"}, + {file = "python_discovery-1.6.7-py3-none-any.whl", hash = "sha256:5422f67e5f533bb02d7b73de1c422ed74037536fd869e05156c07565926ec68e"}, + {file = "python_discovery-1.6.7.tar.gz", hash = "sha256:7f7c28568c8ec3d988d8bddbdbec8e65f08e8767e52f734e18b8b498549b9b70"}, ] [package.dependencies] -filelock = ">=3.15.4" -platformdirs = ">=4.3.6,<5" - -[package.extras] -docs = ["furo (>=2025.12.19)", "sphinx (>=9.1)", "sphinx-autodoc-typehints (>=3.6.3)", "sphinxcontrib-mermaid (>=2)", "sphinxcontrib-towncrier (>=0.4)", "towncrier (>=25.8)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.5.4)", "pytest (>=8.3.5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"] +filelock = ">=3.16.1" [[package]] name = "pyyaml" @@ -2530,104 +3049,98 @@ files = [ [[package]] name = "pyzmq" -version = "27.1.0" +version = "27.2.0" description = "Python bindings for 0MQ" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev", "docs"] files = [ - {file = "pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4"}, - {file = "pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556"}, - {file = "pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b"}, - {file = "pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e"}, - {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526"}, - {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1"}, - {file = "pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386"}, - {file = "pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda"}, - {file = "pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f"}, - {file = "pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32"}, - {file = "pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86"}, - {file = "pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581"}, - {file = "pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f"}, - {file = "pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e"}, - {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e"}, - {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2"}, - {file = "pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394"}, - {file = "pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f"}, - {file = "pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97"}, - {file = "pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07"}, - {file = "pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc"}, - {file = "pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113"}, - {file = "pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233"}, - {file = "pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31"}, - {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28"}, - {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856"}, - {file = "pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496"}, - {file = "pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd"}, - {file = "pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf"}, - {file = "pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f"}, - {file = "pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5"}, - {file = "pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6"}, - {file = "pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7"}, - {file = "pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05"}, - {file = "pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9"}, - {file = "pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128"}, - {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39"}, - {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97"}, - {file = "pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db"}, - {file = "pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c"}, - {file = "pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2"}, - {file = "pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e"}, - {file = "pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a"}, - {file = "pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea"}, - {file = "pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96"}, - {file = "pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d"}, - {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146"}, - {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd"}, - {file = "pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a"}, - {file = "pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92"}, - {file = "pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0"}, - {file = "pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7"}, - {file = "pyzmq-27.1.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:18339186c0ed0ce5835f2656cdfb32203125917711af64da64dbaa3d949e5a1b"}, - {file = "pyzmq-27.1.0-cp38-cp38-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:753d56fba8f70962cd8295fb3edb40b9b16deaa882dd2b5a3a2039f9ff7625aa"}, - {file = "pyzmq-27.1.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b721c05d932e5ad9ff9344f708c96b9e1a485418c6618d765fca95d4daacfbef"}, - {file = "pyzmq-27.1.0-cp38-cp38-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be883ff3d722e6085ee3f4afc057a50f7f2e0c72d289fd54df5706b4e3d3a50"}, - {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:b2e592db3a93128daf567de9650a2f3859017b3f7a66bc4ed6e4779d6034976f"}, - {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad68808a61cbfbbae7ba26d6233f2a4aa3b221de379ce9ee468aa7a83b9c36b0"}, - {file = "pyzmq-27.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e2687c2d230e8d8584fbea433c24382edfeda0c60627aca3446aa5e58d5d1831"}, - {file = "pyzmq-27.1.0-cp38-cp38-win32.whl", hash = "sha256:a1aa0ee920fb3825d6c825ae3f6c508403b905b698b6460408ebd5bb04bbb312"}, - {file = "pyzmq-27.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:df7cd397ece96cf20a76fae705d40efbab217d217897a5053267cd88a700c266"}, - {file = "pyzmq-27.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:96c71c32fff75957db6ae33cd961439f386505c6e6b377370af9b24a1ef9eafb"}, - {file = "pyzmq-27.1.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:49d3980544447f6bd2968b6ac913ab963a49dcaa2d4a2990041f16057b04c429"}, - {file = "pyzmq-27.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:849ca054d81aa1c175c49484afaaa5db0622092b5eccb2055f9f3bb8f703782d"}, - {file = "pyzmq-27.1.0-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3970778e74cb7f85934d2b926b9900e92bfe597e62267d7499acc39c9c28e345"}, - {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:da96ecdcf7d3919c3be2de91a8c513c186f6762aa6cf7c01087ed74fad7f0968"}, - {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9541c444cfe1b1c0156c5c86ece2bb926c7079a18e7b47b0b1b3b1b875e5d098"}, - {file = "pyzmq-27.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e30a74a39b93e2e1591b58eb1acef4902be27c957a8720b0e368f579b82dc22f"}, - {file = "pyzmq-27.1.0-cp39-cp39-win32.whl", hash = "sha256:b1267823d72d1e40701dcba7edc45fd17f71be1285557b7fe668887150a14b78"}, - {file = "pyzmq-27.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c996ded912812a2fcd7ab6574f4ad3edc27cb6510349431e4930d4196ade7db"}, - {file = "pyzmq-27.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:346e9ba4198177a07e7706050f35d733e08c1c1f8ceacd5eb6389d653579ffbc"}, - {file = "pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6"}, - {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90"}, - {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62"}, - {file = "pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74"}, - {file = "pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba"}, - {file = "pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066"}, - {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604"}, - {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c"}, - {file = "pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271"}, - {file = "pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355"}, - {file = "pyzmq-27.1.0-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:50081a4e98472ba9f5a02850014b4c9b629da6710f8f14f3b15897c666a28f1b"}, - {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:510869f9df36ab97f89f4cff9d002a89ac554c7ac9cadd87d444aa4cf66abd27"}, - {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1f8426a01b1c4098a750973c37131cf585f61c7911d735f729935a0c701b68d3"}, - {file = "pyzmq-27.1.0-pp38-pypy38_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:726b6a502f2e34c6d2ada5e702929586d3ac948a4dbbb7fed9854ec8c0466027"}, - {file = "pyzmq-27.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:bd67e7c8f4654bef471c0b1ca6614af0b5202a790723a58b79d9584dc8022a78"}, - {file = "pyzmq-27.1.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:722ea791aa233ac0a819fc2c475e1292c76930b31f1d828cb61073e2fe5e208f"}, - {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:01f9437501886d3a1dd4b02ef59fb8cc384fa718ce066d52f175ee49dd5b7ed8"}, - {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4a19387a3dddcc762bfd2f570d14e2395b2c9701329b266f83dd87a2b3cbd381"}, - {file = "pyzmq-27.1.0-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c618fbcd069e3a29dcd221739cacde52edcc681f041907867e0f5cc7e85f172"}, - {file = "pyzmq-27.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff8d114d14ac671d88c89b9224c63d6c4e5a613fe8acd5594ce53d752a3aafe9"}, - {file = "pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540"}, + {file = "pyzmq-27.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:480dba27b145373b5e103890f17969d891bc9e86746d6b8b29dd70b0d4addc62"}, + {file = "pyzmq-27.2.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:722f0a6940be1a483c81029a271d950e04dc2ff113a42e21b3d2b7a0d8e59638"}, + {file = "pyzmq-27.2.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ee556ed1cf836f96de9d5e545563116426d4a94f21b8041fdc79408eff18ebb"}, + {file = "pyzmq-27.2.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d64da42cae09e6b0c61368b4cc8ca80f23ce3af17584d08053f3dc957433d5ed"}, + {file = "pyzmq-27.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:376981d106598beb70be384f44d8f589832fd0051d184d38d10043da3cc3b080"}, + {file = "pyzmq-27.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:40d96cb7a8f6a43aa9617c00215c2b73e1b5e4a1d6cbc9f5860ed7ac682599f0"}, + {file = "pyzmq-27.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4ebc7889b31bc11c72e9f17ba3ebb0a8b0911cce413f41b498e55383a94819a3"}, + {file = "pyzmq-27.2.0-cp310-cp310-win32.whl", hash = "sha256:650c6cd7cb39a069e7048261efe66fce8bf2e0052c831a7a099b7a0f2ea860d7"}, + {file = "pyzmq-27.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:82a09aa67871d4f2fcafd47bf670fb93210b232a7c2d4b8a54676314edf04033"}, + {file = "pyzmq-27.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bad4813f270592cedf56977e31ac1fc374fb0f6f67ea5134a5e37c19cb429a8e"}, + {file = "pyzmq-27.2.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:9216132843d139a123f243c07fe70f7487dce5041093dd77040f9adb5dc91872"}, + {file = "pyzmq-27.2.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d41ebb260b69329b7d4a2936d44c872c86dd785355b51366c8b14e07ed7e9373"}, + {file = "pyzmq-27.2.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:468139ddb2e494d06e586bd3a6835077e8b3764560c8db552fe685c5867fc24e"}, + {file = "pyzmq-27.2.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39755dc4a923021bd0677990ffdbc21cff0e1ee1cf07fe3817acea153ef4cb67"}, + {file = "pyzmq-27.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:714f8cbd66c7e405338d668f79d2fe83fe923defe348e843be998603cf92eeff"}, + {file = "pyzmq-27.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1132805970045adb9f5f05dd57040978286a8e21a5475f2c2ddf1bc983b9a2c7"}, + {file = "pyzmq-27.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b26f2d0493b79ce3c3112c8a12649418915582ba4707b8ed9f44febf2be71f42"}, + {file = "pyzmq-27.2.0-cp311-cp311-win32.whl", hash = "sha256:44f261eca7dfb9904ea2b56428f59ab693bbe2715c0413a701f17b067ebf877c"}, + {file = "pyzmq-27.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:8b86e04f55af0f4d8cd8ecf14c0b8b81ebc8fd66fa20126b753514628ecadc7e"}, + {file = "pyzmq-27.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:917d601e9540098f580d2723d0ce6402cdb6f02bc8dc2de74e0dca6e13bffd1b"}, + {file = "pyzmq-27.2.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:591c8de5851c5ea372194469fe97587b97c3b641e9a70f31bb3474acbfde0241"}, + {file = "pyzmq-27.2.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:00e73942ef12cecbc7951c4a9104bb8ffaed742abb13af2da6833d90dd368cef"}, + {file = "pyzmq-27.2.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f8079d0521fe94bbb401fe9407578b28f3701627c8be2c9f7e0c5b77dcb0109"}, + {file = "pyzmq-27.2.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dea74fd65f1fc5f7fe167916a473ebe6ed6174e5e5d9de11ea6583661be6cf43"}, + {file = "pyzmq-27.2.0-cp312-abi3-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcc99ca132b667a4ed750afd42db4ea73288f18425a9b2e3c0af095665c491f5"}, + {file = "pyzmq-27.2.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b8d5f66e4a8246cf77f7b8f7902af64f00553368fa0373c89d99b78f0ad79394"}, + {file = "pyzmq-27.2.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:d1526b42a2e725b84ed226f37becedc250c6347594e5ed304e4e9aff68c9aec3"}, + {file = "pyzmq-27.2.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f707bcf2c1d007d14d70531d4dd7b41060881c73efa845580bf6faaf9ea24d42"}, + {file = "pyzmq-27.2.0-cp312-abi3-win32.whl", hash = "sha256:fdaaa4ea3242f6ad298eb5177eb042aea5c73c30e76d20caee7b15af20d24ec2"}, + {file = "pyzmq-27.2.0-cp312-abi3-win_amd64.whl", hash = "sha256:2c218c6ab8bc447ba62054b581fd30209689d199c6ecb253f79615ca74a38e12"}, + {file = "pyzmq-27.2.0-cp312-abi3-win_arm64.whl", hash = "sha256:348d6fd3e4b81ae4580622ea8c2ea60224e84b2ac1b3be4482e6edc7de06e7a3"}, + {file = "pyzmq-27.2.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:c551b9e2f86dc625fcb1a032c0d68042678caf96a8dd7c28796766b673bd5b52"}, + {file = "pyzmq-27.2.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:288cc790da0e3064a14a38ddc56ba169dada8c8af4cb86518db2bcbd380eedbb"}, + {file = "pyzmq-27.2.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:3d45189c0c3c99f817b7fefff0d32eeef684cf33e1e3c0fc4281515357c54702"}, + {file = "pyzmq-27.2.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d61910b52be5b2cd8b248dbcbe3a1b0275556a7d99fb613fc43323b546e273b8"}, + {file = "pyzmq-27.2.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:3ab6eb88590e510ab16715c32dbba12000da9bee989fdadd9ee19a234c492eb7"}, + {file = "pyzmq-27.2.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1ecbdd131b9669f62d3a45afee5527c7ae9f141e4301267f21714c90bd21725f"}, + {file = "pyzmq-27.2.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3146385b94a760236c5eceff468a66a296a716ca98a2e0f9217b1518118466b1"}, + {file = "pyzmq-27.2.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9846e881620dd62566ca76a53e384c3f37490faf4b9240aebc7498810dfca853"}, + {file = "pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d9527e3dbaef1edaeeb2446fa7379446814a43ade8adc7c4a5ebe69437815ddd"}, + {file = "pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:56b48fa9d478a3af7254f397697a62f5ad3e1bb677e200b2701f0c290d97e5af"}, + {file = "pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bf0b6e4ce1bb089751c504c5493d6b0557eabd02dd21b76e9086cf964234b103"}, + {file = "pyzmq-27.2.0-cp314-cp314t-win32.whl", hash = "sha256:fba8afcf265c6e9fbe1594cb045d4765c6c9a7d607653a8196067ef23566b843"}, + {file = "pyzmq-27.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d1bc1d380a91d954ed5fc9f12915dba014eed0978d2de05ee7ca688bdaac144a"}, + {file = "pyzmq-27.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:c7cfb75caa83f5153c687e9d2107f64b5ef0ef0d6edd260d3ff920baaaa69101"}, + {file = "pyzmq-27.2.0-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:c5129a8fe43ecc49b99eb75616603d483a3c2fcaef504988fafe8ea392aea98b"}, + {file = "pyzmq-27.2.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:baa2ce3485145653194d6c8c5beedd1e9f0bf46a0919c9fa2fe2204fc35b74d9"}, + {file = "pyzmq-27.2.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:e1ed46048d1920cabc96d952a0d5cfe4127ad8db572c335aae4e3c57b9278d7f"}, + {file = "pyzmq-27.2.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e0fa0bc6b1a184aee59b32efcd1b7f0e6d5b8f9387799e4c16a4cb66a86747d6"}, + {file = "pyzmq-27.2.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4bd6743e8bf854c3bfce892dd6578a514aabf128e37a4b2eafcf01856f7e44"}, + {file = "pyzmq-27.2.0-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95369ed6626afcfe2ac89832fb1b917c077fbeb905fbbe5d918349ce0222b89b"}, + {file = "pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:40124779c3a56ad5d91902df1ff89159cb414b6c1a0ee697abcc66cf5e6db62d"}, + {file = "pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:ec8a318dfc27c7d946651b3d9e8025d5734f30c168a822195601827207bac09b"}, + {file = "pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:88c0fac061bac269076edeb3a209acefc96cd6167c239daf1c2b404ac48d7012"}, + {file = "pyzmq-27.2.0-cp315-cp315t-win32.whl", hash = "sha256:ac126d48cf18aa955daabef43bf0009ff76ad4deee437d09ecf15388214b5beb"}, + {file = "pyzmq-27.2.0-cp315-cp315t-win_amd64.whl", hash = "sha256:edce90a1e588ec63adbf612cc0ad582de4169cd216c7ae53c15f42a2ee902f35"}, + {file = "pyzmq-27.2.0-cp315-cp315t-win_arm64.whl", hash = "sha256:a843094b4d3d633bc3623e47a2ff50742d6af02bc1f7606aa2e67e971e21878d"}, + {file = "pyzmq-27.2.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:76afba06ae698f2b8fe4fb34b32c760a650f168c2e622f370f2c528035b7f650"}, + {file = "pyzmq-27.2.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:c9322f9c87b0935870516c2876e1e29497fdc50439c785ece63e3fbbab06c821"}, + {file = "pyzmq-27.2.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7e2579c5de82ddf4544d723c1bc8b44c3b806d157acc9fb2a2d18e10ef28e202"}, + {file = "pyzmq-27.2.0-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:95f52b877149b06bbdeec2e8ea6230aad14950bbfbcfa16e7eb88951f07d6b28"}, + {file = "pyzmq-27.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a0ee3c49be2aa15abd12cbbd14d4ea2892f872c688e1e487af39ec1972ed549d"}, + {file = "pyzmq-27.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6eb63cc61ab93b01b9afc887a160255e2fbe703fdbacfe5feaef87214f51bd6c"}, + {file = "pyzmq-27.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f52f08101907609cc08db6a1f9f2a7a9afd54e9b2ca16178c9c38e99fb593cef"}, + {file = "pyzmq-27.2.0-cp39-cp39-win32.whl", hash = "sha256:507c0b33f95502723d325487e8e50c2cdd3b37444143f05423a3861327f69bf7"}, + {file = "pyzmq-27.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:ff60f0f7ccfda0e303ac43bec7096007b7cdf2c41b3739d1ec667febe67acab3"}, + {file = "pyzmq-27.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:d61a0169ba05ab7ebc48dc793f092df12f789bf378dac8321ccd966fd93d94e8"}, + {file = "pyzmq-27.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:770a37f28ddfbe1d2c40a2e3ce37e5fd10831daa6ae9634105aa8a5d23507b00"}, + {file = "pyzmq-27.2.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b398c5fe102b41e1559f7ffdae760aabd5f432d73b047b4ae0eac4e01cb594d2"}, + {file = "pyzmq-27.2.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0e1af01858d6dc0c09cea57f9cb1ddf4601f04897b6bb1efc3a2038123c87d79"}, + {file = "pyzmq-27.2.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:211350c3ccd4746bc5a85e8fe961bad1f7f2f274f67cf1f785fad7f96f562eea"}, + {file = "pyzmq-27.2.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dde5e291548ca0f397623b5e523db5c90172b32aa4fd3ba464a79ea31a580b43"}, + {file = "pyzmq-27.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:94242bd4de6af7e74665e14a88630bccd615057f6acfaf08a3a432551d604645"}, + {file = "pyzmq-27.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a7c1144dc61777938e932a2c9011b980b89fd8ff3733033b34c44c299187a6e1"}, + {file = "pyzmq-27.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:c218b816220d05acf6ab1bafca58926d95cbcc5fec5024724666030466308f0c"}, + {file = "pyzmq-27.2.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ae6ebbc0bfe5a21ce21e32ba567bf73df2d93888109c65acbd42506cf9395759"}, + {file = "pyzmq-27.2.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:679b5b1dde326a921ea2c9ec1f9ea3115bfe1b4735779bbc6eb0473a0ed93f71"}, + {file = "pyzmq-27.2.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5c6d8744d10b5e1eadd90a7c58f8546acf6bf680ee463f7e6ada09ad6c9f802"}, + {file = "pyzmq-27.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3ee8dd7031d5e23f632e0e7eee67183ca7d2536e0de35dc1e5d69f3471a791e8"}, + {file = "pyzmq-27.2.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:97d4c6622f129b514a4f5939af1b5f434c97f47085d9311b5f7f36e24b3bd447"}, + {file = "pyzmq-27.2.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:dfcd024eade5870b25f890c4df0ba9421ed8167d8d3d82334237512c1158dada"}, + {file = "pyzmq-27.2.0-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:a070a9cdad1f8f8a85ea153afcc4654f11b10895d14c0acabe10f1df0e0892ea"}, + {file = "pyzmq-27.2.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:010db74a1dd67c7cd8b8b30916355735db7d633a070510bb34e41ab679ab2c0e"}, + {file = "pyzmq-27.2.0-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ab72ee77b313d0658447204c8201f9b315146e923b48c56ea7dbd005d464a91"}, + {file = "pyzmq-27.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8a5c04ad2e368142aea52d1abdf6631cb2534864e3c16ab78268ab957060b2a6"}, + {file = "pyzmq-27.2.0.tar.gz", hash = "sha256:54d4259d1bfae24ecdb5ca79f7acc2eac6c286a02d6a0ae617797cb45f0726d3"}, ] [package.dependencies] @@ -2700,14 +3213,14 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] [[package]] name = "requests-cache" -version = "1.3.2" +version = "1.3.3" description = "A persistent cache for python requests" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "requests_cache-1.3.2-py3-none-any.whl", hash = "sha256:c52666c76b08daa94d05a99327dd24afc46f405abc044e8c2267b540f90673d0"}, - {file = "requests_cache-1.3.2.tar.gz", hash = "sha256:bdc3680931f98a1dea509d339ea6b45cea526945b47b250ce63ffd2744ee0b14"}, + {file = "requests_cache-1.3.3-py3-none-any.whl", hash = "sha256:c8df20ff874ebfc026959e3874e6c12bd6724934cdb10925915908453d4b17e4"}, + {file = "requests_cache-1.3.3.tar.gz", hash = "sha256:79b72d5ac5143992d1836ad78f4d8e65666061dd44e220548caab3723089826b"}, ] [package.dependencies] @@ -2728,34 +3241,33 @@ yaml = ["pyyaml (>=6.0.1)"] [[package]] name = "rich" -version = "13.9.4" +version = "14.3.4" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" groups = ["main"] files = [ - {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, - {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, + {file = "rich-14.3.4-py3-none-any.whl", hash = "sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952"}, + {file = "rich-14.3.4.tar.gz", hash = "sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9"}, ] [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rich-click" -version = "1.9.8" +version = "1.9.9" description = "Format click help output nicely with rich" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "rich_click-1.9.8-py3-none-any.whl", hash = "sha256:12873865396e6927835d4eabb1cc3996edcd65b7ac9b2391a29eca4f335a2f93"}, - {file = "rich_click-1.9.8.tar.gz", hash = "sha256:4008f921da88b5d91646c134ec881c1500e5a6b3f093e90e8f29400e09608371"}, + {file = "rich_click-1.9.9-py3-none-any.whl", hash = "sha256:365e7a9d0adb42e41ea832a0a12e02c44c079a26536dee688125eb9814f97274"}, + {file = "rich_click-1.9.9.tar.gz", hash = "sha256:324cba7513cd4187ee92b2eef21f071714e45be062458c8b157bd7e0c81103e3"}, ] [package.dependencies] @@ -2764,10 +3276,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} rich = ">=12" typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} -[package.extras] -dev = ["inline-snapshot (>=0.24)", "jsonschema (>=4)", "mypy (>=1.14.1)", "nodeenv (>=1.9.1)", "packaging (>=25)", "pre-commit (>=3.5)", "pytest (>=8.3.5)", "pytest-cov (>=5)", "rich-codex (>=1.2.11)", "ruff (>=0.12.4)", "typer (>=0.15,<0.26)", "types-setuptools (>=75.8.0.20250110)"] -docs = ["markdown-include (>=0.8.1)", "mike (>=2.1.3)", "mkdocs-github-admonitions-plugin (>=0.1.1)", "mkdocs-glightbox (>=0.4)", "mkdocs-include-markdown-plugin (>=7.1.7) ; python_version >= \"3.9\"", "mkdocs-material-extensions (>=1.3.1)", "mkdocs-material[imaging] (>=9.5.18,<9.6.0)", "mkdocs-redirects (>=1.2.2)", "mkdocs-rss-plugin (>=1.15)", "mkdocs[docs] (>=1.6.1)", "mkdocstrings[python] (>=0.26.1)", "rich-codex (>=1.2.11)", "typer (>=0.15,<0.26)"] - [[package]] name = "roman-numerals" version = "4.1.0" @@ -2925,143 +3433,141 @@ files = [ [[package]] name = "rpds-py" -version = "2026.5.1" +version = "2026.9.1" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.11" groups = ["docs"] markers = "python_version >= \"3.11\"" files = [ - {file = "rpds_py-2026.5.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3397a5ed7174dc2786bb214030232fc36fe8e5584fec43a9952cc542b1a12036"}, - {file = "rpds_py-2026.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:99ab6ba7bfa2cb0f96a04e3652355bf04e3f51aceb1e943b8541dab7ba4828cc"}, - {file = "rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0efbe45632665e53e3db8fe1e5692db58fc5cb9bab4459d570b83efefe11164"}, - {file = "rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:01d17b29c0c23d82b1f4751147ec49cf451f1fc2554eb9ef5f957e55d2656ead"}, - {file = "rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7559f72b94ae52659086c595dfa017cde03155f7832071d30959049052cb3ece"}, - {file = "rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e25b7088f9ccbfc0dfcaa52bf969300ca229e10ecf758974ebcbb080a4b37bb"}, - {file = "rpds_py-2026.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613fc4ee9eaef26dc5840666214dd6fbcebcf32f46e76f4abc473059f4e13dda"}, - {file = "rpds_py-2026.5.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:85264a90ff4c05c1568dd65f5921c837614b67c60358fb4c17df3b7f2e90690a"}, - {file = "rpds_py-2026.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe71bca7d547acb17027c7fd1624ff8aae623499c498d3e7011182c4de5c25e0"}, - {file = "rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05fa4f41f37ec97c9c260441a940450a192f78d774d2b097eee1379f1e1246a"}, - {file = "rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df1d2a1996755b24b9ecee92cb4d36c28f86f464a6a173349c26bab41e94b8c2"}, - {file = "rpds_py-2026.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8895840ac4809e5f60c88fd07617cd71326e73d6e5a8aa783c5c0f7c24985de2"}, - {file = "rpds_py-2026.5.1-cp311-cp311-win32.whl", hash = "sha256:3684a59b158a7683aaeb8e25352e9a9dd2122cec78f2d8530266e4f91b4c7b3f"}, - {file = "rpds_py-2026.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:7bd530e6a530bb3ea892f194fafa455f3516ac25ecf7143fd33c09be62b0470a"}, - {file = "rpds_py-2026.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:0a5ae4dbe43c1076983b72616496919872ae7bbe7a1e21cc48336bc3154d130b"}, - {file = "rpds_py-2026.5.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3abe24a66e57adcfa645d718063a5fa5103ecc71ddbf26d78af8f9368018ff1d"}, - {file = "rpds_py-2026.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b1d94308ddf0b1982f61f2eb54bf92997c9ece8a8093ef014250f4a517906c"}, - {file = "rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa92420128dadce7f54bd73ba1825a273e9268fe9e35dbf7e6362890efa4e08"}, - {file = "rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca653c6546386227cd9800d1bef6a348099acf8db4250341da6d90f663d6dfcb"}, - {file = "rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66c93681c4729e4e3ecba31b8179fae083ff3118841672835140338b4b9867c1"}, - {file = "rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ff257542e04796880e011e15cd4dc21c2599975df2aaa8f2c8495ca574e1a5"}, - {file = "rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6825cc329b290e93c5f6a9be2393118a763f6ccf6abd83704e0c102ca583644"}, - {file = "rpds_py-2026.5.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:de42116e69cb53b911cc34aee5ab98f36c597b822545045d49e938818b99e5e4"}, - {file = "rpds_py-2026.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0f920015df2a504bebaba6d4c31ccf3fcf942f92655c086da30b671aad19aa6"}, - {file = "rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0408a24e44feb919423dc6d9da677cb5cddb894d2ca9e763967d156d9c60fab4"}, - {file = "rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cea68bcd53467561ae2f96a6bdad1544299ba97b5b0ddcd5ac3d376e5c781c24"}, - {file = "rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4be8b1d2a705cc37d08256004e1d07de143fa0075c8e85a3df020b776f62b732"}, - {file = "rpds_py-2026.5.1-cp312-cp312-win32.whl", hash = "sha256:6736718bd4fc49cbcb538ba30516fdbef161522acefb739657d48b97bd864fed"}, - {file = "rpds_py-2026.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:0a7d1eec967df0e9b22614a5e177622e0c89611d03727fa0cb48e45028907870"}, - {file = "rpds_py-2026.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1841d067089e117142d79b98aa0df2f08b52f2ecc1819dd2700636c0db74a473"}, - {file = "rpds_py-2026.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d"}, - {file = "rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3"}, - {file = "rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559"}, - {file = "rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db"}, - {file = "rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02"}, - {file = "rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b"}, - {file = "rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e"}, - {file = "rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b"}, - {file = "rpds_py-2026.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46"}, - {file = "rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf"}, - {file = "rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f"}, - {file = "rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89"}, - {file = "rpds_py-2026.5.1-cp313-cp313-win32.whl", hash = "sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842"}, - {file = "rpds_py-2026.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf"}, - {file = "rpds_py-2026.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc"}, - {file = "rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55"}, - {file = "rpds_py-2026.5.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1f2c391c3059798093b65df23aca2cac150460ae9c630d99dec83d703d9485b9"}, - {file = "rpds_py-2026.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:413b424f7c4ee65ab5e5be91f5731be0f8b41a1ee2b12dfe810d716312e95a78"}, - {file = "rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c595a1d9255dce0599e13130d1440ab2506654f2b50294226ee06402f8fef63"}, - {file = "rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c27c5f6102eac8c03e7595a00827a53b271ba40a53b59ff8709170e0855ea4a"}, - {file = "rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c7fcf61d44cacecaf3aea542b0e053db77972a4573e7ceda16fb2b399161195"}, - {file = "rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c817a189d4ee14290420e5ff051e4dd6baa13f3edf84685071dee07a6d538ee"}, - {file = "rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21846aac0ed2e0589f38c12dc44e77bb64e494b771eadbcf169cba00566ba7ba"}, - {file = "rpds_py-2026.5.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b317c87a13f769a4e787819bd508aaa5d69aa09b0880de9af6d3a8a54571cdec"}, - {file = "rpds_py-2026.5.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce87129d9f2c14fa6c4a8601fb80eb4488c80d38a20cd13758ef11123e14995d"}, - {file = "rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9cdddb6c1207d284d94fd1530adf57fbd797fe7c4b8704ba85f49414f2557e7d"}, - {file = "rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4e237e139f94d3c036fd28eb9f564c99055476ff4ff05cd42be55ce349b5aa02"}, - {file = "rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ed0954b524873214369184a9c82b0eaa45a3fbb9a798cd95b17e0d98499e7ea0"}, - {file = "rpds_py-2026.5.1-cp314-cp314-win32.whl", hash = "sha256:2d88621d6a7d4dfa633d21abe90f280bb205274e16b1d1e61c6ad4640b2453b7"}, - {file = "rpds_py-2026.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:cef8ac28d26f4dda3533060c20fbf80a325458fa9fd23ea72a73cdfa8e978838"}, - {file = "rpds_py-2026.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:eaaea962c68cdc68d4a533ba985ab8e9484277910bbfaa2ab3ef7732667bfed8"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:21942f52dbbd5f8758bf021213d28bd45c39e873e65e2407faf5f1846f5761ad"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f414556f6e3958300ff941e40c9f97e3dc9774ddd1b3434c475d73dd354bbed3"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1013a8625c74043210190b246f5b1551e09757c1f356c6e4160ef96c5bc081"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc68e231a77a5f0d774ae278a1f8e55c0456501820847c1e4efb3829f3441df6"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9baffb505aff33acc69b422a19f77806680f3c8632227d79f48de8a810d1c2c5"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8d2f912928d426e8cfa396f7f3f8d29a59e6689c86dcca3c420730c1096322b"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90f628283be835db980c941767d41c9a27b5239e54ba0a9c1335247e82406964"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:1ebb2f0ab7e16132995a72de805170e0203df0c3dd22e1ef1cd1fdd90bd7a131"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3df3d16ded76f1f8c9cdebd0e1ea55fdf4c23b812de189814da7cf229c22a81"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9af8905b8f854990e40d5206aa5ac58d9b0fe0b7f351ff2bb086c20f6c8c6a47"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:036a36a87fb1cd3b214d11c4b3c4f7d2ddad933625dca1c900b56a057c07740a"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-win32.whl", hash = "sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a"}, - {file = "rpds_py-2026.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6"}, - {file = "rpds_py-2026.5.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb"}, - {file = "rpds_py-2026.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291"}, - {file = "rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1"}, - {file = "rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8"}, - {file = "rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2"}, - {file = "rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038"}, - {file = "rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26"}, - {file = "rpds_py-2026.5.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd"}, - {file = "rpds_py-2026.5.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9"}, - {file = "rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14"}, - {file = "rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01"}, - {file = "rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d"}, - {file = "rpds_py-2026.5.1-cp315-cp315-win32.whl", hash = "sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa"}, - {file = "rpds_py-2026.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325"}, - {file = "rpds_py-2026.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-win32.whl", hash = "sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df"}, - {file = "rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:edf2765d84e42447f112ad877af8fe1db0089aaec5b28e88d6eab45e7fe99cea"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad3773236e95f7f33991eb125224b7da66f206504d032a253a02da7e134519fb"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a04df86b3f0fade39ec8fd0e0aab089b1da9fbd2b48df778a57ef96f5e7d38df"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6142dbd80c4df62a5d899f0d616d417f84e0bc8d32526c8e5589019d75d028a7"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b35217adefe87f2fe4db7e9766cabe84744bfe9616d9667be18988928c7f2dc"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b95d5e11fc712b752081183a55a244c03cd00570489edd7014d8899f8ceb8162"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141c9498daf2ace9eda35d2b0e376f9ea8b058d84f2aef4f96fccfd449a2f251"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:6f249f8b860a200ad35193af961183ebe9132710484e6f6ce0cf89fd83c63a9a"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e4abbf391a70be864920858bf360f4fb380577c9a0f732438a1996726e2c195b"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:c74005a7bb87752acf351c93897ec63ad77a07a0da7ecad9c050e32e7286ba34"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:8213afbe8a3a906fb9acb2014423fe3359ee783d0bf90995f70623a3217bfa6c"}, - {file = "rpds_py-2026.5.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8c43a8a973270fd173bf48cdf80bbe66312421cba68d40845034f174f2389049"}, - {file = "rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256"}, + {file = "rpds_py-2026.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2711d29b653b3bce48a63d18b9c6b53274669e6d6c4094dddeb4d9a0e45128b2"}, + {file = "rpds_py-2026.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3231c4c0e521dafa5be0c9f114ee2c2ad46650836f2d72caa86801950c3e7044"}, + {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e01b3c878c8641913e688edd1b3f08658c6783d29cf6b826bd3c0d1ae7a1ffaa"}, + {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3e524c7874ac72884d28e16dd5b8d839fd09e0fe76b020d3fbca23212a7b8c52"}, + {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761fdae6728ceb99ab182fad2f0cc1e262f610834dc891aea1d1a2a2e634776f"}, + {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b723eb406dec5bc9ec516c73ab9c3239a3284e017f7eb89ee2b3258bd504fb7"}, + {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:136a1c3fe4402b7008bc81cb62ee538481795b61a7e83df88dff3b3f02b726ff"}, + {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:839dde845559254f34885267c6878f60d61d5205180226d976fe488d45fa128e"}, + {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56cd8b3f77d7b6812f533b662186a1f28316931166ddc00fb893b1b0db7e9888"}, + {file = "rpds_py-2026.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:074a4d198bc34d9a8ea425114fc3ded6d11ec01f6a314a8db67454a5152d8834"}, + {file = "rpds_py-2026.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6beb738155fe8ab8091afdfa5a3226b21c2b1593f1e50ebb90eb25b44dbc0391"}, + {file = "rpds_py-2026.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57492a550a1d88d29d003247e5f78dd8cf04a701fac0e4c8db8745a6d2504e0a"}, + {file = "rpds_py-2026.9.1-cp311-cp311-win32.whl", hash = "sha256:d95a354e02393eada6d7351184671aced9d4cce109dabf927cb7aa99624352a1"}, + {file = "rpds_py-2026.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:ce4d4f52e2a4324396caddbd45a97d8d7be5f42edd25d2355282a9c34f9b2f7f"}, + {file = "rpds_py-2026.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:fdcd198979b4ecffcc1beba366a7fbcf4eb41243691a82fe52ceb0b902f09c12"}, + {file = "rpds_py-2026.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:50906f5aea24b5a865cbd0a589698288631d9f3a54c3a937c83aefa95a0d14af"}, + {file = "rpds_py-2026.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e21c1429e205828ea886a2293a4a2c8e01f4c25d9893ca330e97a6cf73f52e7b"}, + {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693b2728bbcc48d09a981a356954b0c47c53ff25b545856f28a889ea619f69a"}, + {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8601470267d938bcb7f3ab1a336100af51a4fd5b6ed030ef52461bb3ef5e7e07"}, + {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3890a6aa36e6baa53d5258a2a25d3ef8b37ad165a6ab27a892d7c3e3a432cd69"}, + {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b5b393eda5ea42cca1c1a6665f2a4882b4fd5d1777e41ce0545a107fb008c9d"}, + {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:addeda51556dac7c1a2f14cda62db8b621cd12afba3091d03a96c72932387eab"}, + {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d9edf30457d74eebfd76b045535e36f1cd89062566a128a0db2145ca042d787e"}, + {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:815d26356930846a40c7bc1366e7b1b0320ab8a063e66c11298a208bed0fd237"}, + {file = "rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b5a6f40f0a1486b4b36c888123afc67acdbd9f33235927acf5ff295429a0ba3"}, + {file = "rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b5b8b0753718d258fd454283fbd57e14545d3b40583fa672e27cb4f987626bcc"}, + {file = "rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:46d80bc76b51a6c24f9944368c28d38b8bcbcea1da4f2f8d3ebc31a67e8c6ec6"}, + {file = "rpds_py-2026.9.1-cp312-cp312-win32.whl", hash = "sha256:befc2d6a953e563f8a7bfd87a42c22ebf8a3e980dcb7b6a4d17b70b0e914e8a3"}, + {file = "rpds_py-2026.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:5ce8943f79c2210f7abcc28e86367b03b28d95027fd01c46d2472373ae70c86f"}, + {file = "rpds_py-2026.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:501909f2e4a1e2dee528ef766fe3c469060ebc17e54a8383d404ba07a81a6f02"}, + {file = "rpds_py-2026.9.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a36b70596407634ca82d4b989a3729074a008537a0522e4c8046a67c729103e9"}, + {file = "rpds_py-2026.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eba5d173f7d5708b22a93815017a4611873ed54db9f268077c0dd1ed99cfc858"}, + {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:457866b85daf5034296666168b84a69e0b2e89dc4f1af102b46f6448a60b9063"}, + {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a3a52a3ba86436ab3aef510fbe21512abc2ddd1993005dfe50514bd2284ef025"}, + {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7841166b7fa64c9c56404617ae4341448847482d45933b13135d26c130519e5"}, + {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:926bdd3e3b5998ddf70cc64bc8cf57209571f9044542913afb673799fec77dd0"}, + {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7868b85224291c6cb6759f9b5adb9745f486d226f62b16a614dd5a2a5ab2b35b"}, + {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3cd182d7291d29b92c521a0069d9c01ba6193628a9a105531d11b40a6d731a33"}, + {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e6ea1cda8d8c688278430e4268a42f5e5da3bdd74578dfadc0820c3f1766ce83"}, + {file = "rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5943980471829f6de242a20b109de3111ba6b77e3af0ffc587028ac854b05e6c"}, + {file = "rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:76d3af9732d2dab69f28179b40ba2d87e2f1d5824b4a694780aa787d685e8f36"}, + {file = "rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:78326f4cb4427a56ba4996c0762b63be45f06b85f086526420d2b3a66e40f84d"}, + {file = "rpds_py-2026.9.1-cp313-cp313-win32.whl", hash = "sha256:172e47169583f46ce118cbec68e6795d0da0f4606b488b6434f8276bca0a058c"}, + {file = "rpds_py-2026.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:3e93b2cd69a9830be33e03945cd7cda940a0a8bfcfbff41d6144f0cb0d3d8bd9"}, + {file = "rpds_py-2026.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:d151e148117294133bf8af7eeace085e7e87432db15ab6adf640330298a47f6f"}, + {file = "rpds_py-2026.9.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c9d1aca01f49170fdcf5c92761b1fafe97f554b721ca4570c5949fff778f0d4b"}, + {file = "rpds_py-2026.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3f0e9ac28fc067d4d34b88ae43c48e9489455c97fee9633d851f7eeed5a05d35"}, + {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07deecbfce94c78473018bc7d10b337cc651d12df87a1eb2cb3e4024bc9c33d0"}, + {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:821b2755db9194409254012f429c56643416fb96ef9be090be82ec8826b7f477"}, + {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c91c210ae7645626c608400e3519b4a642f837cce09ca830db3beb2e9f274d4"}, + {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54ac2158a6f96cfbabff0b2eedaf94b90c5ec7ca8317fcadc61e1c2b2e0ff6ef"}, + {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac2f5dbafd585dfe31f86a23ebf0d3ba480a9d49ebc87947267b5608d4ea0cd"}, + {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:8aa5dda18d39b6143eb24809d158f9252c88f402749b6f1b62a506cc7d96cc35"}, + {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c90e7fa02e8f5de0d10c17595c568ada48c5302e749462c0ea1a4c362111a86"}, + {file = "rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6d198bad4e49dd6732fbd636e2fc5c082f45c8cad0b4acb756b00c82c76072e"}, + {file = "rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:96beca19ec79de272e8668585380ff9092c47077c1d7a1e098e00bbd921f4785"}, + {file = "rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5cf77eb04f20b720be95265a3e00eb2a14814074255cc27069c551b2db53118"}, + {file = "rpds_py-2026.9.1-cp314-cp314-win32.whl", hash = "sha256:a03d57b86d2a51d0a66c92177e2be154ad015f357791d306e714569999cdb4cc"}, + {file = "rpds_py-2026.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:837c6b305e26fe0f75b15c92cf3b2ba29e0ae19dc40b1c557b026cb426347d0c"}, + {file = "rpds_py-2026.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:fce4b85234a0cbad67bf8e6e1201ee815d172c9aebad75f25645bc4d834f8e31"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3a72c11530d71abfb66c8d7696a2f86c43e63fca8b948f1a784ac490f4ec688e"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:068c37bba854ec2fe42f7365c640af11dd9895890ccbf2df5070d0c059bd7f96"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fca4eb6df565e2a928f1c7dad92d27db8f9df0f449e76423ed5d7e713ed445"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c933c6678c6f116ff8af47a4c6db0868b8ace74af0343016c0ef00f00272ea69"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:028ad274ea951dac64491b5d1e65712a4aeabfdbdb9fccf797b57bd899b0c495"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:740d0a99cf9de0b17a3943388e9294a59becf75e7c43421f387bd3c7a9901f7c"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0da298fb372dc192610a4b9ecbc68a0cd8b675bbbd1fc519d01b41cfd658333e"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:eb61be926bb81567c1f48bdc8aa22b9855048dc2efd53871f9f7e6e9a5632346"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:42e75466f83cd43f6026c81eab74246efb2bdadafb307b85700632d06c68f299"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:617f59cde379b4f648a09797b7f683d04b90a46344cddab85639da5aff0f5531"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3edae8c5ddfdb6985d49ae9d150516e5076888879022f91a26c2de9276ce0bdb"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0f045bb053c9057720d72c56dffe30dffdc05997b2897a827b9325f0ab6623fa"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-win32.whl", hash = "sha256:bf35d0568abda97233239ce32896d3ad53fccc537832c104e30c94aa5fb93569"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:1e8d4d79d828299bf44a55db22a9388ab967b49d17132c88eab0f4360b48da8e"}, + {file = "rpds_py-2026.9.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:1d77b649e6f7cdf12ca5c2a98dad0ad37f9ea9b6f960408a92f0cb12bb3d04d9"}, + {file = "rpds_py-2026.9.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:00ba2d8c7dd4ee537978ddf4b3fbd712bef2d8751603f7f3146b3f4287768e25"}, + {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec450527cbf485e13c8d3602a54f428ab0432fdade0ede75efd74b735421c871"}, + {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:306ee1850d8105b5baf977e78d45fcadd12c1a54678d614c9baf217708446e91"}, + {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef6b65b03247c54692ad4fd9ee97cb772781927db72e3cb05e70b3db6d1ff14f"}, + {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a575404ebc9cf2e91edd32eaf570ec1430eb900d4f56724ba7dd4bc1fc9c176d"}, + {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c16ab111bc27c646ba8aa005d0527754edc538ebb636f0b1bf8e244b48d1945"}, + {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:7664419f27db41d4f1c43a78dccda7dd6e8ef2428df3ee01d0c2a07a6b071297"}, + {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4b26b03d9d2658ee2fa234f8f4f19f38a09773fe5261028025032e26d4d35af0"}, + {file = "rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:be3e47e2d91aa3942ff9bf4077a505226005abfc39b6f7554a91c1b9393986b9"}, + {file = "rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:6307a0da524939decb8ca4a3933b8ab62525794411d6984fca6726e732804af6"}, + {file = "rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:159a7aab5c5e8b112c8830f54717ce56da1252ebdbb526f5be2df2309280b9e7"}, + {file = "rpds_py-2026.9.1-cp315-cp315-win32.whl", hash = "sha256:dbc2673f9223d420c91145599b3ba45a8a50c207d1976908e5fb5ddb0c9b9429"}, + {file = "rpds_py-2026.9.1-cp315-cp315-win_amd64.whl", hash = "sha256:75c38c50ab9aca840225d9a9a3810bf11d04bd5c1f186cabbb8aee56db3e9b15"}, + {file = "rpds_py-2026.9.1-cp315-cp315-win_arm64.whl", hash = "sha256:a431156bb41865fc14cd5d79bb9d7bbed83110b0159e34e62ae30951f96c0009"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ef0d8c843e2827d6c120ab4687e9423fb1d893db1df27b7c1506615bcb9734a0"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:45bc6bccf78b20fd834237d18db64965d7ee68ba7f60440a26c7ab71e7b8d51a"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d55198263bb51f557550c6ed2e6d1cb6a6fed6eb5c9120b741c5926bef8a45d"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8763f20692da7df39b0afdd1ba3042b004c50a45994f76c2d9a25641f7673db"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e43d4a1f673e8a1cbd8533e809e02b4bf9d4f2280269bb640436556312121250"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea394a937f17a54c51239348bdbe2e3518124c8d4a8951ba04a311d3095bd18f"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdeaa99ce822dca76cfb1b993e9120c5ea212f2eb66d48950ad63c349668a018"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:b4f062343e7ad3fa94f2c66e5ae667dee47ee74dd41a9057c4fbe163236a123d"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22ffd29a63d71fb1b81552c21f2c2b734949b7ac751a9be70675a939a900839b"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:08dae4a4095150a7c4545a1fb40b98e1ab1744fbc2770d92c977b9dadaa49ab6"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:9a0460d43603d1fd9ef59c30278531e15d78581721ddb538fa560aa7817ea4ad"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2d1f6da5128eabf34e963d7163a818846075a52568250d006c4c953b40f903"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-win32.whl", hash = "sha256:5c6ee90dee3e85e055ddfd502d611643d9b0fd94c818220bda84ec3dacd9b27b"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:fe5ad0664ec772b02c45859041aa17655709cced7a31005817fbbbd988c25567"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c0d2cb595a420b34d5086db0add011e26e2c09d6a024afbac4228bf8f863a30"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3d6ed6a98cfd19155996605474982cc470d7601746a6439078f1a5a3fa8b050"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8171b44a054e5c67fd748ada04187f1250bf35b95f85e52ab64bcf3331a923bb"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fda1d96e542c37b6c804547dbf489c129fe7c97183a76a5ec275909ba1a063df"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:270bdcdaac5d5b6f73c5e22e7e135c7f2a50e789f71d9e241d5be8d90026e19a"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:84a6ecc0c940169190d2c23bd969debd48c94dbc855acd60188a68d71d421608"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab4b2fda7c2b542f7f9d886cc6a838c5079d2b76f72e6081411faba11adde2c9"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:44b32a7c4f0da3d28af31c259e38ddcff096f855e205ed0671d02fcf44f1ea1c"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:a3dbc5ed9514908d5046107d7b1346bde71eea61de6e0e4919c19354f97e769f"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6eae33003518fd4cb4f83a218d5371469dd3001aa3b87128c005b07762f7fe5e"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp80-macosx_10_12_x86_64.whl", hash = "sha256:0483515261947e4e8b8e1375bf7463e7eb6ccfb3d86e7b554d90cd5285f20f32"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:4cfaf02209061880210819934de2f4f6aa83dc04dafe6770276acc240a56da31"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cdc537c8633d7fd92a82e2e0d2ab74320a3f63d5e59fb9cf08711e08fe151c4"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10e208f2425d973938afcd56e28a7c4be32e27b6a60b5d381f49fb9d8acf9759"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c0dbbcc19735fe5f8b0a54c07659d154a9e69f47e15d0a6ab7299215daf62cb"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684fd492fff4fead00587544e059be2bbcb6f93454f21fa2a91b66fc7508be82"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1028417bb44037eb3069c1009bd7b7277212876cda22fbe565b0bca9fab6d2c"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:492e5e428cbe126221611f47e068f01660352feec4ad18bc0f5ea9b2ae88fb14"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:88b5268892fde430d5531f95bc560b6efbbd67c929662c586afd729a96e7461c"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:01445c8d194aa032a08e944f16567672da1c62dbdbefd8b6d0693032e290cf68"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_i686.whl", hash = "sha256:eef6a03b0b6d08d0835ccfa8ec8d1bc70525e3801387567137b50c557695e6da"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6b9bf3135b4ad5981df9a73d71a35272d650a2985ae9c2746357b24d59de2448"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp80-macosx_10_12_x86_64.whl", hash = "sha256:56c6952a9b15047466d0c2347c446a761d4527f89976156341e68f0ce5cc08b0"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp80-macosx_11_0_arm64.whl", hash = "sha256:b242c27c8f836305a4a72df9cdd564386ac57b807bd252a063223331c9316b37"}, + {file = "rpds_py-2026.9.1.tar.gz", hash = "sha256:4793ef7f78268b124b73fa933440f01d258bbae01de9fa53e9080c9ab0425a12"}, ] [[package]] @@ -3084,32 +3590,48 @@ oldlibyaml = ["ruamel.yaml.clib ; platform_python_implementation == \"CPython\"" [[package]] name = "ruff" -version = "0.15.19" +version = "0.15.22" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.15.19-py3-none-linux_armv6l.whl", hash = "sha256:922d1eb283161564759bd49f507e91dc6112c15da8bd5b84ed714e086243cf86"}, - {file = "ruff-0.15.19-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4d190d8f62a0b94aba8f721116538a9ee29b1e74d26650846ba9b99f0ae21c40"}, - {file = "ruff-0.15.19-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5a2c86ba6870dd415a9d9eb8be94d7924ebec6a26ffc7958ec7ca29d4bff967d"}, - {file = "ruff-0.15.19-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b432bc087264aea70fd25ac198918b70bd9e2aa0db4297b0bb91bbfbbc63ce"}, - {file = "ruff-0.15.19-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8530a09d03b3a8c994f8b559a7dcdabc690bcd3f78ef276c38c83166798ebf56"}, - {file = "ruff-0.15.19-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87bf21fb3875fe69f0eacc825411657e2e85589cce633c35c0adf1113649c62b"}, - {file = "ruff-0.15.19-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b229cb3ef56ecc2c1c8ebeca64b7a7740ccaef40a9eb097e78dde5a8560b83"}, - {file = "ruff-0.15.19-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c754515be7b76afe6e7e62df7776709571bcfc1631183828afcf3bafa869e3"}, - {file = "ruff-0.15.19-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a498f82e0f4d8904c4e0aea5139cdfac1f39d19a3c51d491292f63a36e83b2e"}, - {file = "ruff-0.15.19-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d48caa34488fb521fd0ef4aea2b0e8fe758298df044138f0d67b687a6a0d07ed"}, - {file = "ruff-0.15.19-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4171b6613effa9363cd46dd4f75bd1827b6d1b946b5e278ed0c600d305379445"}, - {file = "ruff-0.15.19-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27c15b2a241dd4d995557949a094fe78b8ad99122a38ccae1595849bcc947b3f"}, - {file = "ruff-0.15.19-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ed03b7862d68f0a8771d50ee129980cbf1b113f96e250b73954bc292f689e0bb"}, - {file = "ruff-0.15.19-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:08143f0685ae278b30727ea72e90c61e5bd9c31b91aac4f5bb989538f73d24b8"}, - {file = "ruff-0.15.19-py3-none-win32.whl", hash = "sha256:8f47f0f92952af2557212bb10cf3e695cd4cf28b2c6e42cdb18ec6c9ebfa19da"}, - {file = "ruff-0.15.19-py3-none-win_amd64.whl", hash = "sha256:efeca47ee3f9d4a7162655a3b8e6ee4a878646044233978d4d2c1ff8cdd914f0"}, - {file = "ruff-0.15.19-py3-none-win_arm64.whl", hash = "sha256:6c6b607466e47349332eb1d9be52fb1467423fc07c217341af41cd0f3f0573be"}, - {file = "ruff-0.15.19.tar.gz", hash = "sha256:edc27f7172a93b32b102687009d6a588508815072141543ae603a8b9b0823063"}, + {file = "ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8"}, + {file = "ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697"}, + {file = "ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f"}, + {file = "ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576"}, + {file = "ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178"}, + {file = "ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224"}, + {file = "ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a"}, + {file = "ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e"}, + {file = "ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb"}, + {file = "ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74"}, + {file = "ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c"}, + {file = "ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296"}, + {file = "ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262"}, + {file = "ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64"}, + {file = "ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf"}, + {file = "ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde"}, + {file = "ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661"}, + {file = "ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809"}, +] + +[[package]] +name = "shibuya" +version = "2026.7.12" +description = "A clean, responsive, and customizable Sphinx documentation theme with light/dark mode." +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "shibuya-2026.7.12-py3-none-any.whl", hash = "sha256:0a2c0c109e7cc47fc650b01feb671d267c0ffe2e39c98f3529e32d723a9c9d9b"}, + {file = "shibuya-2026.7.12.tar.gz", hash = "sha256:9496606f5b95595511ad86b773d74b4b8d694a4dcc2f848e6ec34e2ad71d9060"}, ] +[package.dependencies] +pygments-styles = "*" +Sphinx = "*" + [[package]] name = "six" version = "1.17.0" @@ -3136,14 +3658,28 @@ files = [ [[package]] name = "soupsieve" -version = "2.8.4" +version = "2.10" description = "A modern CSS selector implementation for Beautiful Soup." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["docs"] +markers = "python_version < \"3.12\"" files = [ - {file = "soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65"}, - {file = "soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e"}, + {file = "soupsieve-2.10-py3-none-any.whl", hash = "sha256:8596eb8967d744174820280fa62b4542a2e955bfaccca73ed8a13c6eb8e9b502"}, + {file = "soupsieve-2.10.tar.gz", hash = "sha256:49e9380d7d2905463583bafe285e818c7366a9ed7b3aee221c1ac79c905d8bc0"}, +] + +[[package]] +name = "soupsieve" +version = "3.0.4" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.11.5" +groups = ["docs"] +markers = "python_version >= \"3.12\"" +files = [ + {file = "soupsieve-3.0.4-py3-none-any.whl", hash = "sha256:8412826614f9c632227067d0985aeb9f584bd15efc852955430e097a9fe7afbf"}, + {file = "soupsieve-3.0.4.tar.gz", hash = "sha256:924fab84ae21604c19e175e1f39815eb6fa83ba23836e09e9de180925e5bc4c0"}, ] [[package]] @@ -3220,6 +3756,29 @@ docs = ["sphinxcontrib-websupport"] lint = ["betterproto (==2.0.0b6)", "mypy (==1.15.0)", "pypi-attestations (==0.0.21)", "pyright (==1.1.395)", "pytest (>=8.0)", "ruff (==0.9.9)", "sphinx-lint (>=0.9)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.19.0.20250219)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241128)", "types-requests (==2.32.0.20241016)", "types-urllib3 (==1.26.25.14)"] test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "pytest-xdist[psutil] (>=3.4)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] +[[package]] +name = "sphinx-autobuild" +version = "2024.10.3" +description = "Rebuild Sphinx documentation on changes, with hot reloading in the browser." +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "sphinx_autobuild-2024.10.3-py3-none-any.whl", hash = "sha256:158e16c36f9d633e613c9aaf81c19b0fc458ca78b112533b20dafcda430d60fa"}, + {file = "sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1"}, +] + +[package.dependencies] +colorama = ">=0.4.6" +sphinx = "*" +starlette = ">=0.35" +uvicorn = ">=0.25" +watchfiles = ">=0.20" +websockets = ">=11" + +[package.extras] +test = ["httpx", "pytest (>=6)"] + [[package]] name = "sphinx-autodoc-typehints" version = "3.0.1" @@ -3340,26 +3899,6 @@ requests = ">=2.32.4" Sphinx = "*" urllib3 = "*" -[[package]] -name = "sphinx-rtd-theme" -version = "3.1.0" -description = "Read the Docs theme for Sphinx" -optional = false -python-versions = ">=3.8" -groups = ["docs"] -files = [ - {file = "sphinx_rtd_theme-3.1.0-py2.py3-none-any.whl", hash = "sha256:1785824ae8e6632060490f67cf3a72d404a85d2d9fc26bce3619944de5682b89"}, - {file = "sphinx_rtd_theme-3.1.0.tar.gz", hash = "sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c"}, -] - -[package.dependencies] -docutils = ">0.18,<0.23" -sphinx = ">=6,<10" -sphinxcontrib-jquery = ">=4,<5" - -[package.extras] -dev = ["bump2version", "transifex-client", "twine", "wheel"] - [[package]] name = "sphinx-tabs" version = "3.4.5" @@ -3467,35 +4006,22 @@ lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["html5lib", "pytest"] -[[package]] -name = "sphinxcontrib-jquery" -version = "4.1" -description = "Extension to include jQuery on newer Sphinx releases" -optional = false -python-versions = ">=2.7" -groups = ["docs"] -files = [ - {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, - {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, -] - -[package.dependencies] -Sphinx = ">=1.8" - [[package]] name = "sphinxcontrib-jsmath" -version = "1.0.1" -description = "A sphinx extension which renders display math in HTML via JavaScript" +version = "2.0.0" +description = "sphinxcontrib-jsmath is a sphinx extension which renders displayed maths in HTML via JavaScript" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" groups = ["docs"] files = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, + {file = "sphinxcontrib_jsmath-2.0.0-py3-none-any.whl", hash = "sha256:15ca92fe0eec82f0baf91a7efcc51ffacdb605924be6327683a52ebdacb6744c"}, + {file = "sphinxcontrib_jsmath-2.0.0.tar.gz", hash = "sha256:551f780cf9687f9c3c74f0cf9f18aca06e0d6958853453f3803fe4d357c5b924"}, ] [package.extras] -test = ["flake8", "mypy", "pytest"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] [[package]] name = "sphinxcontrib-qthelp" @@ -3564,6 +4090,46 @@ files = [ {file = "standard_imghdr-3.10.14.tar.gz", hash = "sha256:2598fe2e7c540dbda34b233295e10957ab8dc8ac6f3bd9eaa8d38be167232e52"}, ] +[[package]] +name = "starlette" +version = "1.7.0" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.10" +groups = ["docs"] +markers = "python_version == \"3.10\"" +files = [ + {file = "starlette-1.7.0-py3-none-any.whl", hash = "sha256:67f8e99895493dd2911a03f11314af6ceebeae4e704bb9f43dfc6a9db151c93e"}, + {file = "starlette-1.7.0.tar.gz", hash = "sha256:c79f74ea63cff761804fbbfb182f1e0b440c2d07b164d24700c5a1bab5d6ff5d"}, +] + +[package.dependencies] +anyio = ">=4.0.0,<5" +typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "httpx2 (>=2.0.0)", "itsdangerous", "jinja2", "opentelemetry-api", "python-multipart (>=0.0.18)", "pyyaml"] + +[[package]] +name = "starlette" +version = "1.10.0" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.11" +groups = ["docs"] +markers = "python_version >= \"3.11\"" +files = [ + {file = "starlette-1.10.0-py3-none-any.whl", hash = "sha256:478c0ab3b4ced6e82719849c118fa406f0278a3499c1a898cd15cd31bdc38ce4"}, + {file = "starlette-1.10.0.tar.gz", hash = "sha256:b8399098d4c1e3522734f7235dc79b1fc967f96cf139c81e0fa30479e0685f9b"}, +] + +[package.dependencies] +anyio = ">=4.0.0,<5" +typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "httpx2 (>=2.0.0)", "itsdangerous", "jinja2", "opentelemetry-api", "python-multipart (>=0.0.18)", "pyyaml"] + [[package]] name = "tabulate" version = "0.10.0" @@ -3612,109 +4178,182 @@ files = [ [[package]] name = "tomli" -version = "2.4.1" +version = "2.5.0" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev", "docs", "test"] markers = "python_version == \"3.10\"" files = [ - {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, - {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, - {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, - {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, - {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, - {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, - {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, - {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, - {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, - {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, - {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, - {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, - {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, - {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, - {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, - {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, - {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, - {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, - {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, - {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, - {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, - {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, - {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, - {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, - {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, - {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, - {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, - {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, - {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, - {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, - {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, - {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, - {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, - {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, - {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, - {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, - {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, - {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, - {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, - {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, - {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, - {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, - {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, - {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, - {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, - {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, - {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, + {file = "tomli-2.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c4dc1c1781f2f716de763d1e9a7b34c6a894e167e291c7c5d16c72f7a9538545"}, + {file = "tomli-2.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eff8babca5a7999bc137acbc7482a8b7e17ffca5075ab41f5d770ab408c7bfef"}, + {file = "tomli-2.5.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86665cee9c4835b7a7f1e8ec2c719b5258d4dc782887aded5a8ae7352a96843b"}, + {file = "tomli-2.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e369fd63331746182360977b1892bfc215476a30d61612d732425311639f56"}, + {file = "tomli-2.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ad1ea345759240d6463efa0ed1c704402752e49aa21476620738d74d72d8aa1"}, + {file = "tomli-2.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:96243987194634bd411066ce40c952e108f86af04db533ecd8ac3ff2a85b1885"}, + {file = "tomli-2.5.0-cp311-cp311-win32.whl", hash = "sha256:610b27d99f28ec5f191c7064a48f3ddb179a1fe6ca73d571483ae859f57b605e"}, + {file = "tomli-2.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:c804ae44fe7b4bab5da295e4f980a1ff04670bca9d23fe0a4e887e08ebd741a8"}, + {file = "tomli-2.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:cfac177ebd6236003846ea339981f71457cb6eb748f23381eb257e45092e3980"}, + {file = "tomli-2.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f4a40d03fb9f63424f0979855bdeaf44dd7696b8d59501822c10ed30ba532df"}, + {file = "tomli-2.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ebf8d19b17bd0daeb7b7dec81a946a439b753942fd0210d6e96c532249eea6b"}, + {file = "tomli-2.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf0b5e8e0f68ebb494356e577c06c139161efd8d3b9050f93b39b7c26cc54ff0"}, + {file = "tomli-2.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cf74416bdc94ae458b14e37286c1073081850ac8459a00d0c5efef5d44294c6"}, + {file = "tomli-2.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:61ea1ebe1e55a34ea8199cc8dbff398d35027b82271c8ac4802fd3a1fd5b1bcc"}, + {file = "tomli-2.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed53f7e89bb04f6d9e8e7799112360b0c4d5cbff067de0814c98c37c39b920f7"}, + {file = "tomli-2.5.0-cp312-cp312-win32.whl", hash = "sha256:e7ad033e27a516a233bea839cdb77b80146facb3b4f40bf02cd0cac165cdd5c2"}, + {file = "tomli-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd05de8c1698f8413dd7d869492693a0bf2211543b787ac78cd5e7536af1a6d7"}, + {file = "tomli-2.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:069435bd5480429b98c5e5afb02ab21c219b6f0064680671c6dc0d46817346ea"}, + {file = "tomli-2.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:943276cf269e0071948d9ff697159c1735e623c1151d88abb09b74659ef0cbea"}, + {file = "tomli-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:463b16086865b97facd8d0b3fb4cb7c544e3f58d2a69dc3113d6db9653fdb043"}, + {file = "tomli-2.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1245a6638fc4bb0a60af38a7d45413db34a13842027c77597c712c998c62fdf0"}, + {file = "tomli-2.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d8bac3d603c97e6854424e5b2b5b741bdbde387e09f162fb0446812b4a8362b"}, + {file = "tomli-2.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:21e4cae4114aba25aa0d4f85cdf486d290fb35c0954d7bba536248da64d43066"}, + {file = "tomli-2.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bbaefc84548d754be821bba7c4141c4787dda182f9e77f2f87b71213529efa7b"}, + {file = "tomli-2.5.0-cp313-cp313-win32.whl", hash = "sha256:abdbf6313b8d9efe157edeb7ab6eae4de064b1300ad31abf73755154b30abe68"}, + {file = "tomli-2.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd4dc129784e0c5335bd4e61dfcc4487499a013419e655cf2da1d091b7e0efdc"}, + {file = "tomli-2.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:69491c143d2fe063046e0301e62a810bed338fa4d1ce0fd870c27dc1e09b0d84"}, + {file = "tomli-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d3182ee2d887e507bd67319a0a61105d1dd33facc111329559a233b772c1a105"}, + {file = "tomli-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:521345fd1f19d45b8df87657aaa38b6f2ca3800059fadf428e7ebf479a383646"}, + {file = "tomli-2.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e95c7614e705bfe2b04b27aa124adec59752d15813df37e2156747cab3a006b"}, + {file = "tomli-2.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ac2027d37c3afbdf4bdd377f2676f6f1d2122a5be1f1137b49dced590b37e75"}, + {file = "tomli-2.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c414be4ed9d3cac80c42e348fa5a956117d1a48227f48026e31f59cb4a7671eb"}, + {file = "tomli-2.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9b03d7dc168353b4132965bde20feceabaa470e570c6f59660dfae59b1f9eeb3"}, + {file = "tomli-2.5.0-cp314-cp314-win32.whl", hash = "sha256:6f041843c4d3a37245c0c056fd955b186bf8b1fb85690cbe40b81230891dc34b"}, + {file = "tomli-2.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:f4b653094e18f9031102d3a1da5c729c8f222d85225b18037dac621695e46e1a"}, + {file = "tomli-2.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:3f89d10c1ff6a38d992c27fc8a4816af71a909e08a40ec66934240b1e74347c3"}, + {file = "tomli-2.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e9e15b4a6c7dd6b85b5fbab29488a73f1f70de516942308daa266bf0e0aeb0d4"}, + {file = "tomli-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e12bbcd32897272fb05929110362ae9ff4c1b9bb26bd9e971e71dcd3275b4c3d"}, + {file = "tomli-2.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20aa36de8f2cf87237143bc1fa1aae8d6612c09118f4da21c6a684db5dd1f6f9"}, + {file = "tomli-2.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22185fad8a1e622f064e78008018a0dd3323550dcb479cb7a1d296888d74024f"}, + {file = "tomli-2.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:984012f71908165449a951de2050d52f276bfe3aa5d5f570f63ddad814370374"}, + {file = "tomli-2.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f79203b3965b4000e91808aaa7c040206093f2b8bf86f455982f2274c9ccf442"}, + {file = "tomli-2.5.0-cp314-cp314t-win32.whl", hash = "sha256:91294a9fb94a75542f6e46e4a2ae709bd8d9b51134098cae5cf3bea5478b6d03"}, + {file = "tomli-2.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f15e3e0b835a6d68b10c86bf80a3149780498d6911c93c3ffd1861d19f9200f1"}, + {file = "tomli-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6664b7ae7af7294256c53960a6103077f4914cec8ff98479c352f622c6f6b2f0"}, + {file = "tomli-2.5.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a525685c2f97da40762b8695eb7aa0af4c8344ca1905c73e4e29cb04d34607dc"}, + {file = "tomli-2.5.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:9dbb18c1cfb2f6517942fc9314437f66aa06d94436ffb1f06102ef3572f35276"}, + {file = "tomli-2.5.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:752e8b1aa6a4367ef8bf6a1a1e005540f7ed055ba36d7193796812ca5404eb52"}, + {file = "tomli-2.5.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47300f9bf791808f77d82747691c4bb09cb14bdf3060cca99b42cdc4361d5a7"}, + {file = "tomli-2.5.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:19b0dd8749f4ea2f112c5fcfb3c5248390c899d7e2e173f1d91abee1fa0ff391"}, + {file = "tomli-2.5.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:57b1c3b01fab802e2899bc3d168dca320e14165e2fd9fd584760fb4ca5826859"}, + {file = "tomli-2.5.0-cp315-cp315-win32.whl", hash = "sha256:667e521b37a6c5ccaa044202c235b530f90177ffe2cd4a64ecc213c7dd535feb"}, + {file = "tomli-2.5.0-cp315-cp315-win_amd64.whl", hash = "sha256:d747252933c8a65ef6bd8da0fbb7ce28a90eb6119d8cd00772cd528aa07b68d5"}, + {file = "tomli-2.5.0-cp315-cp315-win_arm64.whl", hash = "sha256:75dbcde8751b0a960aa3de173aa5e894d590755c6d7758b7e774c06f1dc3cbdd"}, + {file = "tomli-2.5.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2419c2a189551987b59d80e63ec355671283336f41c6b9b89462df679c7d0c57"}, + {file = "tomli-2.5.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:0dc598040da8d42cf20f0be588ed7004f46db12a0ac6c32e03a59dccedaaadcd"}, + {file = "tomli-2.5.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49096930c8d886c9bbdab62d2d0d17ce823ddeea522309a190b36245d5b49e01"}, + {file = "tomli-2.5.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8ade5023067f99fe72b88accd30d0ea05a158e9e32a11f124e731ea9695313f"}, + {file = "tomli-2.5.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b69564772b5c8f22ea5f498dff08cfa825045b4d4c4400529000bdf818aa3b2a"}, + {file = "tomli-2.5.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8ff3a2ca028c7eee0c777f9a092038d0a594a9fa04e215f929a22c329e2cb142"}, + {file = "tomli-2.5.0-cp315-cp315t-win32.whl", hash = "sha256:62fc1bc8eb03e3a9cadfca713d65614ed8e09d974a283295ffe3a831976b4dc5"}, + {file = "tomli-2.5.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f3fcbc57b1791fa6cbe5d8434179d51de12be1a4811469529f47f6e7487a2571"}, + {file = "tomli-2.5.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d2ba24db8a9376921b5e87b4762b9adb0f3f1deaea68f2b8b0bb2c11efb9c3e7"}, + {file = "tomli-2.5.0-py3-none-any.whl", hash = "sha256:32a7b79ac57a2e83670ce329ccf675798bc5a2094783a63676866b70503f2e2b"}, + {file = "tomli-2.5.0.tar.gz", hash = "sha256:264507556cd8b8c8e7c6ee037cdf443a463f03f4c958e57195e3d369711b8ff6"}, ] [[package]] name = "tomlkit" -version = "0.15.0" +version = "0.15.2" description = "Style preserving TOML library" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "tomlkit-0.15.0-py3-none-any.whl", hash = "sha256:4dbc8f0fc024412b57ced8757ac7461305126a648ff8c2c807fcb8e133a78738"}, - {file = "tomlkit-0.15.0.tar.gz", hash = "sha256:7d1a9ecba3086638211b13814ea79c90dd54dd11993564376f3aa92271f5c7a3"}, + {file = "tomlkit-0.15.2-py3-none-any.whl", hash = "sha256:05562412163a3037b94497385fb7f9204c637bc605af7a48199e41cf2272f437"}, + {file = "tomlkit-0.15.2.tar.gz", hash = "sha256:83e32755ba1d20d9340bd069ee97e9f6232ba2346b63ffcba425de01f7e327a6"}, ] [[package]] name = "tornado" -version = "6.5.7" +version = "6.5.10" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." optional = false python-versions = ">=3.9" groups = ["dev", "docs"] +markers = "python_version == \"3.10\"" +files = [ + {file = "tornado-6.5.10-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9261783640e23258694a9ff0795df430a5a7b0a651d3dd53dd0969ad6be16da7"}, + {file = "tornado-6.5.10-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:83e6cf438b106c6b3852d70960967bb1b70c87438050dca0981e4b9aa751a4c1"}, + {file = "tornado-6.5.10-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bdf942448169e5336451d0494d7e3d81cfa726d5aa312affdc4682dd62a62f6d"}, + {file = "tornado-6.5.10-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69acca6501eed74582b76dbbceee2a91613f54728e3e418346000d7103101676"}, + {file = "tornado-6.5.10-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:66aaa3f57d30c6e6becee83ff28055d5930ac724214bde99393eefda83d5e015"}, + {file = "tornado-6.5.10-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4bd192b959f9128fb99b8898148070ba4574c9589b78bce42d1851131fe85828"}, + {file = "tornado-6.5.10-cp39-abi3-win32.whl", hash = "sha256:302eb1e0e3e159314eb591920529fdea80acca92df5510a2cec5bbd4f099ec72"}, + {file = "tornado-6.5.10-cp39-abi3-win_amd64.whl", hash = "sha256:37ae8f150cecfdbf747fc4e12f5e9a97ecd8cf1d4cdb3f119e2de84b11196918"}, + {file = "tornado-6.5.10-cp39-abi3-win_arm64.whl", hash = "sha256:ce045d3c298fddd30e89a2777f97039d1b641eb9518ac7b26a4721903539c694"}, + {file = "tornado-6.5.10.tar.gz", hash = "sha256:a6b1ccd08c04b4a06fb5aeb381be99de5ad1e5375c1785e31d78c880feb57687"}, +] + +[[package]] +name = "tornado" +version = "6.6" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">=3.11" +groups = ["dev", "docs"] +markers = "python_version >= \"3.11\"" files = [ - {file = "tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163"}, - {file = "tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100"}, - {file = "tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972"}, - {file = "tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b"}, - {file = "tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92"}, - {file = "tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5"}, - {file = "tornado-6.5.7-cp39-abi3-win32.whl", hash = "sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4"}, - {file = "tornado-6.5.7-cp39-abi3-win_amd64.whl", hash = "sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4"}, - {file = "tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796"}, - {file = "tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2"}, + {file = "tornado-6.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:01abf15429d64f04ccfafc238b191cc2c177108b7a331f4461707ad77198c8c2"}, + {file = "tornado-6.6-cp311-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:11bb97c00981eb8254e55bd6a5ad960c983ae343fef8ae3ae5584cd1fb70236a"}, + {file = "tornado-6.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e152a3b2d3eae113b836d979823f7bb2320f7e334a84449729964921f374a4c"}, + {file = "tornado-6.6-cp311-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e912d4c4babd04db6f34b25b748174a4a176debe86c8610beedf1815f9af7fdd"}, + {file = "tornado-6.6-cp311-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33aabe8a03d426cef4e0fe5669f0643cadd983b54957f1a7ac2789ec326e9644"}, + {file = "tornado-6.6-cp311-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9aaf0e1c2fb9f046f30e2036b8d2060666399e8464d0a72769e43c754089567b"}, + {file = "tornado-6.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61fc5513cea97720ab9bf54b04827ac0f4500c63f0b072b963d6145c798b4661"}, + {file = "tornado-6.6-cp311-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:31e1a0d5cd704d2fe8a008c1cc27a0e2b67269ca263630a27985dddc160b5537"}, + {file = "tornado-6.6-cp311-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:74416e41e9bdf70fbc2fb242f6fbbc7a58ec11e59d39f15fc5a421ecb6845118"}, + {file = "tornado-6.6-cp311-abi3-musllinux_1_2_s390x.whl", hash = "sha256:8cdd69104ae9d705b3f04d2563aa96e357043e267a455802daae9a1e633a37c8"}, + {file = "tornado-6.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fc7c8a3263501fbe97518cd699101bc3c5f499a7dd4f75fc6cbb3bd9ee1db1f1"}, + {file = "tornado-6.6-cp311-abi3-win32.whl", hash = "sha256:b54ca69ff93d0374efb083a12a1e9ec2dd2036e8496d197e0e8ef77b8d9135c3"}, + {file = "tornado-6.6-cp311-abi3-win_amd64.whl", hash = "sha256:48f26ed9d7beb64e5f06adc864524e581ab6d62ba8ff46eaaa1ac2d0e6d753fd"}, + {file = "tornado-6.6-cp311-abi3-win_arm64.whl", hash = "sha256:29114ffd4e4de869d390393df9e9a0f191065aae1a96f38a58d35e464c04a867"}, + {file = "tornado-6.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:63fc8d243b52fd8dd9ed4ad02d0df08f2024f9aea632a433a8a6c91075076d44"}, + {file = "tornado-6.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d4b85cac795bfc6a7f20ba81871abb8c2ae8fba019061b8a9ae1c2f97e29637"}, + {file = "tornado-6.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e035c065edadfa696529d05eebcbeb25154c129b5aee6dbfb697a6abf5d4f697"}, + {file = "tornado-6.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5b94e589ab855090c448b5c0937b3a75f1c1a604b7b36207e8236d209f720b9c"}, + {file = "tornado-6.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1a394c8c9db9137f2a2b42869d9f7b91398fcf19da120d0dde4c4546172710f7"}, + {file = "tornado-6.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0215d1df6e4b5e736ddad5adf0af4138cf7437fdd39c77c5e69768e37850a632"}, + {file = "tornado-6.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4d711790e0a9576ad924d942c4607e4e401d697588ea6f30f6662824008a7add"}, + {file = "tornado-6.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:034a66bae0f55421d007803e2385d2a59707d8829b2f026373d42f08256e9922"}, + {file = "tornado-6.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5cb38483b077129056a94dd415a71d29ab4e42b46da894fee8b4657003f10b21"}, + {file = "tornado-6.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:46bbbd107efa37777ac4b83601866d19b4dbd5b770a7a9123582633d95959ad3"}, + {file = "tornado-6.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b85e9d0263cdbc027b95947c8b873a7bc7fd6a08d16bfb4f5047350b0808e464"}, + {file = "tornado-6.6-cp314-cp314t-win32.whl", hash = "sha256:4276eef0880e5fe4488e6799c2842f59af3b06637e9313ab4fb506f9c9c2d6cb"}, + {file = "tornado-6.6-cp314-cp314t-win_amd64.whl", hash = "sha256:10d4af98c6df09f8429b1340ccaea3a72887b86f695dfb8056bda517c8115a00"}, + {file = "tornado-6.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b1f0ef20a5278854f5fdac31637bafadb055405df28b7fc945e54add12b172e3"}, + {file = "tornado-6.6-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:8f8e382da30fe042f27f27a871e21aefc2613870e5a41648de18446c72d6ea59"}, + {file = "tornado-6.6-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8c7a8a328396c2195454a2ecd9499512441bbce99505544a717e6a87d16ff6f9"}, + {file = "tornado-6.6-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca025bb0da81c294aef26f27427316df1525220820315f4bf32dc1e84a4d2232"}, + {file = "tornado-6.6-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:59c22ebc905ac2a267f8d9d8a878f86afbcbbde329d75c485f1422fc45a81b83"}, + {file = "tornado-6.6-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f89802bc0bcd15f988d9325b4e6d2171bb04ea68d188732556335e10d3fd00bd"}, + {file = "tornado-6.6-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:70043256d5958f9c655066153d23d60ea3e29bf9854f7fb75850e223c477758a"}, + {file = "tornado-6.6-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:82b49495e3849d644b841ef33b35491160055997deaa93720ff647ef019421db"}, + {file = "tornado-6.6-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:07b2bcaa4bf447bdbfb664e4c28344226540db3f0ebccd74886528338a240765"}, + {file = "tornado-6.6-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:c36b921374c302e54909bae17d9f354061d80e7ae7d8c94f73abbcc217866460"}, + {file = "tornado-6.6-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:7412d9e37aa9f7048e4b591f520a95f7a097278ffab15f1ddf3f5c2320cf6648"}, + {file = "tornado-6.6-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:15bc95112b2d24b0bfaee9a004e3f974da929e33b4244a7f695a22f45b08a5e5"}, + {file = "tornado-6.6-cp315-cp315t-win32.whl", hash = "sha256:f661c21117b875d612371f3eb243d0c69f56d9bdd6aee38b7c24453419dfe554"}, + {file = "tornado-6.6-cp315-cp315t-win_amd64.whl", hash = "sha256:ee8853ab35da510dc952879ebd9995c82bc10ffb7925fd87930dc0d6916f4b39"}, + {file = "tornado-6.6-cp315-cp315t-win_arm64.whl", hash = "sha256:7de62e2dfdf559fd7154d2b9aa58ff3d9cff8304ae0519341feb082618bc2a33"}, + {file = "tornado-6.6.tar.gz", hash = "sha256:34c4aa916d7f2b2d5cadee2c6850f5a4a7f3d688cbf58399ea9bd3057af4abd5"}, ] [[package]] name = "traitlets" -version = "5.15.1" +version = "5.16.1" description = "Traitlets Python configuration system" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev", "docs"] files = [ - {file = "traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92"}, - {file = "traitlets-5.15.1.tar.gz", hash = "sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722"}, + {file = "traitlets-5.16.1-py3-none-any.whl", hash = "sha256:f775618166caa0396c8e337099240f2bd3e5e917d203b2e6fbe21a58d3cb1f6b"}, + {file = "traitlets-5.16.1.tar.gz", hash = "sha256:ed900c2b631aa3a112811139fa97b8d2c3bad5e989656bba4b7e52c7852c18c1"}, ] [package.extras] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=3.0.3)", "mypy (>=1.17.0,<1.19)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] +test = ["argcomplete (>=3.0.3) ; python_version < \"3.12\"", "argcomplete (>=3.5.2) ; python_version >= \"3.12\"", "mypy (>=2.0) ; implementation_name != \"pypy\"", "pre-commit", "pytest (>=7.0,<10.0)", "pytest-mock", "pytest-mypy-testing ; implementation_name != \"pypy\""] [[package]] name = "types-toml" @@ -3730,27 +4369,39 @@ files = [ [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" groups = ["main", "dev", "docs", "test"] files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, + {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, + {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, ] markers = {test = "python_version == \"3.10\""} +[[package]] +name = "tzdata" +version = "2026.6" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["dev", "docs"] +files = [ + {file = "tzdata-2026.6-py2.py3-none-any.whl", hash = "sha256:0a10eec6e13ef16b627fabed9a5f2ba61a62b1c67f327305f4e7e412a70b4aec"}, + {file = "tzdata-2026.6.tar.gz", hash = "sha256:85d2cd9c09f81f0bb29db27759fc83af4f1e620efb814f8c5bf8b4cc4feece6d"}, +] + [[package]] name = "url-normalize" -version = "3.0.0" +version = "3.0.1" description = "URL normalization for Python" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "url_normalize-3.0.0-py3-none-any.whl", hash = "sha256:95234bd359f86831c1fd87c248877f2a6887db2f3b5087120083f2fffcba4889"}, - {file = "url_normalize-3.0.0.tar.gz", hash = "sha256:0552cbf2831a32a28994a13d29bca58a60e10ff6c0380e343ec6d1c2a0d232d8"}, + {file = "url_normalize-3.0.1-py3-none-any.whl", hash = "sha256:97ea68fc543b1fc9f270f34c90cf164453e7d490da2ec653dcd8ebd4e3ac1faf"}, + {file = "url_normalize-3.0.1.tar.gz", hash = "sha256:1655cd214159d9d47dc37aa6ce993c2149da44fa35cac6bafd90036a4eda3ac3"}, ] [package.dependencies] @@ -3761,14 +4412,14 @@ dev = ["mypy", "pre-commit", "pytest", "pytest-cov", "pytest-socket", "ruff"] [[package]] name = "urllib3" -version = "2.7.0" +version = "2.8.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.10" groups = ["main", "docs"] files = [ - {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, - {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, + {file = "urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3"}, + {file = "urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63"}, ] [package.extras] @@ -3777,60 +4428,443 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] +[[package]] +name = "uvicorn" +version = "0.54.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "uvicorn-0.54.0-py3-none-any.whl", hash = "sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf"}, + {file = "uvicorn-0.54.0.tar.gz", hash = "sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +standard = ["httptools (>=0.8.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.20)", "websockets (>=13.0)"] + [[package]] name = "virtualenv" -version = "21.5.1" +version = "21.15.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783"}, - {file = "virtualenv-21.5.1.tar.gz", hash = "sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8"}, + {file = "virtualenv-21.15.1-py3-none-any.whl", hash = "sha256:0d14a991689c67600316880eadc0a37b7690cb632e3b4ac67e2bc05b2b2e24a4"}, + {file = "virtualenv-21.15.1.tar.gz", hash = "sha256:e79c90b8f601701599c25905e02839924fede2c798d52325670d24fbdaab74ee"}, ] [package.dependencies] -distlib = ">=0.3.7,<1" -filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""} -platformdirs = ">=3.9.1,<5" -python-discovery = ">=1.4.2" -typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} +distlib = ">=0.4.3,<1" +filelock = {version = ">=3.24.2,<5", markers = "python_version >= \"3.10\""} +packaging = ">=26.3" +platformdirs = ">=4.4,<5" +python-discovery = ">=1.6.1" +typing-extensions = {version = ">=4.16", markers = "python_version < \"3.11\""} + +[[package]] +name = "watchfiles" +version = "1.3.0" +description = "Simple, modern and high performance file watching and code reload in python." +optional = false +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "watchfiles-1.3.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:000b9688fc8133037a8b075c8ebf98f32844ff8964dda61e85c1db547dafc441"}, + {file = "watchfiles-1.3.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:bbc1198edfdc90fda0600f825aa94150f428dfcbf8138746f55998e0e660d64c"}, + {file = "watchfiles-1.3.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af9c464d410e8c44b58ddf1cbec7c3a02c1cb2c6c80df9c74659ba2fbda0ab5"}, + {file = "watchfiles-1.3.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb94e40b9a0db19636b5e3fed0a9803452ecc7381df13b385272abb779ee83ca"}, + {file = "watchfiles-1.3.0-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7398930a2b76b9dc67a4e6b8bfc7baf30946892a86f417a3920c07bdd6febc30"}, + {file = "watchfiles-1.3.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:025b108f2d5cc2799cb941f79380df3e8b22bfb589c218e801767fe78d598c95"}, + {file = "watchfiles-1.3.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be9ef3cd403d756a304a0a08a112163c63da7ec72c8c9d083c98818291ff8f29"}, + {file = "watchfiles-1.3.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5768b49e426fd5b550b012c866db347cdf15c398ef98dc557b6e6b72fa74cd1"}, + {file = "watchfiles-1.3.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:d36c72fcdc08f143d87316a4c4c3b064e340da2c7383d39803950740ed418935"}, + {file = "watchfiles-1.3.0-cp310-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f0c9865240e065a2247f4b5248528ae3d01e0e8ae071462c250c884dc16e3ee3"}, + {file = "watchfiles-1.3.0-cp310-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:380f513d26cc2e598266b88d66456e07d67ef4be8f0f435a1154d9c44b43d509"}, + {file = "watchfiles-1.3.0-cp310-abi3-win32.whl", hash = "sha256:509d9f74d2bec5c1f4cd868ddcfe0c9601f7ea53066dfcb19d8ede742c4de661"}, + {file = "watchfiles-1.3.0-cp310-abi3-win_amd64.whl", hash = "sha256:1acabde19b67e673274e89a04d613b7fb1f122e2c4b8ec9a581e59c149264b61"}, + {file = "watchfiles-1.3.0-cp310-abi3-win_arm64.whl", hash = "sha256:d978cc1dd7ba44f5590d7de74f7a9c8be7262d9e469c2c0efb8bbfc2574321dc"}, + {file = "watchfiles-1.3.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e0204f90677b7fb8d3a824279a71a01166bea9f6e53923b8056592d87b33b0b6"}, + {file = "watchfiles-1.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7c1125e99f84934a92996ae343501285617373acd66f16b59fd5654b27ad8d80"}, + {file = "watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a85e243c87109d9f48a2e0d10d6abdee8768cc114848b32281ec83d7f81d86ef"}, + {file = "watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd538c71766c59c732e2c5cedc39323c99c11897b99f93eac773438e54af206a"}, + {file = "watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da213822ec9082b62cadf3008e07a8c690ec7a3c89f6dc5e55a8cf07b56a1b26"}, + {file = "watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4593392e87669836670f87d62fc754059eaeb0e6157af44060a46558cebe5a3"}, + {file = "watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26b81bc515a0f03f66a69a6fea86eea54c8eca4173c46dd7222569f79dd2f977"}, + {file = "watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca39661934749df580d89f3dde266f2af7c6968f4c21c72ce2dc387d299e6aa7"}, + {file = "watchfiles-1.3.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:04c5500faa725d69a99b0f63750e80795e455bb951849e9c98a811b4324997c4"}, + {file = "watchfiles-1.3.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:ec3cd4bb181a7b6329134204c2277c7144ab5f62cce9c5335d6f901ee7ce8d6a"}, + {file = "watchfiles-1.3.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e5871d4a7f788a9f64fb06f5793389d29d171e5c32d37c3ebfd520b017cc7694"}, + {file = "watchfiles-1.3.0-cp314-cp314t-win32.whl", hash = "sha256:81c989d7267bfb7b676cd32c83bec97914e396870983cbf493e29415037bff5e"}, + {file = "watchfiles-1.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d754e049006e81a9be49dc78ba7802081cee426e968cbc63ee19e164fa2d0e39"}, + {file = "watchfiles-1.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a5b631db08fd3032db57ec6a92d1777cd60f88803287e4e941a4858c53aab1e0"}, + {file = "watchfiles-1.3.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:45726c6b5a8d67c8087eb2933c4398948546d04206ef40f629dc4881d65d23d1"}, + {file = "watchfiles-1.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6398d250994baff581147ccba846223076fe6492ca93c0d1b3588fcf5355eb5c"}, + {file = "watchfiles-1.3.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:591de392458bca26f6130fea4e667c06b657e873ad334ea06fad5a79f5086cef"}, + {file = "watchfiles-1.3.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec46a1ff4c1d81405a5fc645abdbff5e4e9388f7c9a6cdb392316a7c29b884f"}, + {file = "watchfiles-1.3.0.tar.gz", hash = "sha256:99aee4a07847c06820765fd7b1b49ceac4f3f711ccb7d104655a33231de1c207"}, +] + +[package.dependencies] +anyio = ">=3.0.0" [[package]] name = "wcwidth" -version = "0.8.1" +version = "0.9.4" description = "Measures the displayed width of unicode strings in a terminal" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "wcwidth-0.8.1-py3-none-any.whl", hash = "sha256:f453740b1e4a4f3291faa37944c555d71056c4da08d59809b307ef4feba695c8"}, - {file = "wcwidth-0.8.1.tar.gz", hash = "sha256:faf5b4a5366a72dc49cad48cdf21f52bdf63bdda995178e483ba247ff79089b9"}, + {file = "wcwidth-0.9.4-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b7969c9aa258d536df54c8ac8af5cad0c74a6268c9972e1fb60a5f29feffb66d"}, + {file = "wcwidth-0.9.4-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f279f32c8e206a9092fa350e53ca0c2bd8072e45b1c9fd31bb6d476b59b591c4"}, + {file = "wcwidth-0.9.4-cp310-abi3-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a1526cd25c6420e5153bb7d764c7509e692215ee3ad9b725db1ad665d47eae09"}, + {file = "wcwidth-0.9.4-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:093469e01d881bff3db93ff3f56c364dc87c441ea01f9f41c22de0c0bdf5c6ef"}, + {file = "wcwidth-0.9.4-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b185c8b927b23af2721be1783499d1cfa5c028b098b0858951b4ae224477f1c2"}, + {file = "wcwidth-0.9.4-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:17619066f95c10f44d7ae08ee456602456bc66cc9d5e91966ec73d799acebc5f"}, + {file = "wcwidth-0.9.4-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:b52f265a694dda6b3ad9050c8bd89158ad519109ef7c5eb110b906cc49f25763"}, + {file = "wcwidth-0.9.4-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d8905f1390d828d745fdd1904374c90386d902d3cfa9ee88e9875099f75a4e17"}, + {file = "wcwidth-0.9.4-cp310-abi3-win32.whl", hash = "sha256:4831662c396dd4c59a016ec53c0ac96738006d653a00482777d9a2e2ea323d62"}, + {file = "wcwidth-0.9.4-cp310-abi3-win_amd64.whl", hash = "sha256:f28d77153303450ee13ebbe888297e75cf532c754555b6cabafc9bdda16c421c"}, + {file = "wcwidth-0.9.4-cp310-abi3-win_arm64.whl", hash = "sha256:2b58fc8cf2de1bf9cdb68b8beaa5be198742414a7c8cd97aa51ec039783ffa49"}, + {file = "wcwidth-0.9.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:87e4101789415a6cf55ec4c53eceeb4782c2654c88692941cb588f19dca1959f"}, + {file = "wcwidth-0.9.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ab1e5190fb82476e75fbf963e9d59b4fb1c1e1f4ced82069a0c989016aa959f7"}, + {file = "wcwidth-0.9.4-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:8f892713ab5795d3abae181e9c02db0eb24de4084f9ee080535369d3d98d7bb5"}, + {file = "wcwidth-0.9.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:befbf46467100e43fd8c8cc0c031a0105f13f5418c3be95fdcf0c47214487967"}, + {file = "wcwidth-0.9.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c4b0dd04cca520792100f6110d10be262c94b285a281d1759c551f159214633"}, + {file = "wcwidth-0.9.4-cp314-cp314t-win32.whl", hash = "sha256:14410eb73178cc492aa9bf22f7c12e3bf930e5470b7220cf6f530efbfa2e03f3"}, + {file = "wcwidth-0.9.4-cp314-cp314t-win_amd64.whl", hash = "sha256:c36954849aafae1092d6f9de9548c5280c1d6841637a27ca288b2241c802c4ab"}, + {file = "wcwidth-0.9.4-cp314-cp314t-win_arm64.whl", hash = "sha256:79e09e9824c88f5e275ca99e921685567a5c5c1bb6165c16e27ddd1adab33a95"}, + {file = "wcwidth-0.9.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:a8c9ce14c6b4a499e61de6de7e9e456de11be8bc991554e2ab2ed4feb40b9664"}, + {file = "wcwidth-0.9.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d68f09093c9bec9226a52edc47487042ed4f64c5df91d06f00419495071c0279"}, + {file = "wcwidth-0.9.4-cp315-cp315t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d76b7c715c5ca0103b28915f699f1020ae21f06f1d22cdd85a7a97f7b1536eed"}, + {file = "wcwidth-0.9.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:29c0685f5f5f8815b88308fdcbe2cd12a92996a21a5b1e18352d165174ef7a6d"}, + {file = "wcwidth-0.9.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e05458491759aa42b7b85f36345b3e6df082d6d2d9bf616299668fd598e0209d"}, + {file = "wcwidth-0.9.4-cp315-cp315t-win32.whl", hash = "sha256:9e5090d253f5df99659cddfd3ed90dd21f6ff51989b0421331a63fb2681cbe27"}, + {file = "wcwidth-0.9.4-cp315-cp315t-win_amd64.whl", hash = "sha256:12786620375dc83fabf4b2b2aa52416fef2bc6094522ea29c8a53c75b0dfe9b6"}, + {file = "wcwidth-0.9.4-cp315-cp315t-win_arm64.whl", hash = "sha256:2cca7b5d0b9d0ae49068d3ce806e965326a987f555b62eaa322223396e10d01b"}, + {file = "wcwidth-0.9.4-py3-none-any.whl", hash = "sha256:901eace6d2dd25ebb917d0ce5db18e5c537e1504c905cf32f0d006c12098b05d"}, + {file = "wcwidth-0.9.4.tar.gz", hash = "sha256:fd8d57d3cdfe570e3265c8fab1885c24e34ef40889c0759c0c304a19e9835954"}, ] [[package]] name = "webencodings" -version = "0.5.1" +version = "0.6.1" description = "Character encoding aliases for legacy web content" optional = false -python-versions = "*" +python-versions = ">=3.10" +groups = ["docs"] +files = [ + {file = "webencodings-0.6.1-py3-none-any.whl", hash = "sha256:7fab6269c8bf237c657876b52058ccb182e861518d1c695c1a9aaa8c1c105d5b"}, + {file = "webencodings-0.6.1.tar.gz", hash = "sha256:565f9ad031c702dae404e27a099e3e09186a3ab1b9520f06d215502b651fd910"}, +] + +[package.extras] +doc = ["furo", "sphinx"] +test = ["pytest", "ruff"] + +[[package]] +name = "websockets" +version = "16.1.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.10" +groups = ["docs"] +markers = "python_version == \"3.10\"" +files = [ + {file = "websockets-16.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:49ae99bdfcae803a885c926bf14f886196e84925395bb3f568fef5c0f0979d7d"}, + {file = "websockets-16.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5bfd1ac19b1b9986a9c95a82d5e23a391ebb09e12c34d7be6094b86efcc35731"}, + {file = "websockets-16.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9246a0d063cfcbcc85f2359dd6876d681213f4790832272aa16641b4ed5d64d4"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1214e673c404684b9bf7154f5cf43b45025b1a6160fac3a9e438e9c1a97e22cb"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90001d893bc368e302ef168d82130b4e4fdd27b85fa094682df9b667c2d48838"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:130937b167a52af203c8d58e78d67705874e82759862e3b9671a452fec4abc87"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c9f23004a3d40e89c01a7955d186a6cc83418d93b749701944ce2de3e95a1f3"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f55f0b01956a094c8587146d9558c91937e78789c333860ffaf35931a6e5dbc4"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6aaface73b9c71974c6497366d8b9628357f6c9749e09c4ea3610176c63f2ae3"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc0fad4933f427acd5b1cec210f3ea6dce7089e1724e4b9ec6ef47c6c04d1b3b"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f2769a0344a09e9ccf5b3cce538bc75a51b53eff3275d3896310c8552049195d"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f70541f3104339f59f830522d94ebadb1bf47426287381623443d8bb1cdbf33d"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:dc385593a42e31cd6fb60c19f0ecb015b386603818fc2c6c274fb42bd2bb4165"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:387e8e4aa5df2f90b198fa3cad3478822a89cf905b6a6d6c97dc3664689640cc"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fd46fff7eb62c24804d234f0051c7a8ea81285ad63e0337d3dcf33ca82aee58a"}, + {file = "websockets-16.1.1-cp310-cp310-win32.whl", hash = "sha256:7883388947767080f094950b342b30d35a2a06b849cd967c422fa0db72b40ea9"}, + {file = "websockets-16.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:d57685547e0060cc6fd90ee6a28405d6bd395e525545f13c8d7cd99c78afd79f"}, + {file = "websockets-16.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c"}, + {file = "websockets-16.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a"}, + {file = "websockets-16.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81"}, + {file = "websockets-16.1.1-cp311-cp311-win32.whl", hash = "sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57"}, + {file = "websockets-16.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a"}, + {file = "websockets-16.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00"}, + {file = "websockets-16.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b"}, + {file = "websockets-16.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3"}, + {file = "websockets-16.1.1-cp312-cp312-win32.whl", hash = "sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747"}, + {file = "websockets-16.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7"}, + {file = "websockets-16.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1"}, + {file = "websockets-16.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df"}, + {file = "websockets-16.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87"}, + {file = "websockets-16.1.1-cp313-cp313-win32.whl", hash = "sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea"}, + {file = "websockets-16.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68"}, + {file = "websockets-16.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8"}, + {file = "websockets-16.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293"}, + {file = "websockets-16.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9"}, + {file = "websockets-16.1.1-cp314-cp314-win32.whl", hash = "sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328"}, + {file = "websockets-16.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc"}, + {file = "websockets-16.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573"}, + {file = "websockets-16.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999"}, + {file = "websockets-16.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737"}, + {file = "websockets-16.1.1-cp314-cp314t-win32.whl", hash = "sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7"}, + {file = "websockets-16.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231"}, + {file = "websockets-16.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869"}, + {file = "websockets-16.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9"}, + {file = "websockets-16.1.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e"}, + {file = "websockets-16.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d"}, + {file = "websockets-16.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5"}, + {file = "websockets-16.1.1-py3-none-any.whl", hash = "sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3"}, + {file = "websockets-16.1.1.tar.gz", hash = "sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57"}, +] + +[[package]] +name = "websockets" +version = "17.3" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.11" groups = ["docs"] +markers = "python_version >= \"3.11\"" files = [ - {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, - {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, + {file = "websockets-17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:45cc1ccb12ced6d242335159e8221733c8696da5ad0b6ceef6debb0f5ce89110"}, + {file = "websockets-17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46ae05aa1aecd47d4d7de0f05ce2d1d2eb418f8de614964d754cc3b9a409c400"}, + {file = "websockets-17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43f270a5f21e12db08c73d83849a877c02ab7b7d0fb6e2718b5101d9c810212f"}, + {file = "websockets-17.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d875224bac1a05a20acade268a7a0e3498e7f8cf890acb7c2b23bacd2bd3659"}, + {file = "websockets-17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ad409b7ea102339041281117cf2dadfe71edbb653dc48037cc55d516fb7f448"}, + {file = "websockets-17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d43c311f3519fb1057b6114dc24e9060bc3fb6aedf4aa11160484154b66b63"}, + {file = "websockets-17.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9eacb382f400bb5865e7e6d24bbe709a84510291e707f169dd4a24ee4a5ac6a"}, + {file = "websockets-17.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f09f145b89ea292cf6c25fd7c8c0d90207fd6a9e54c7828499669996a1e5bdcc"}, + {file = "websockets-17.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:49fb5b4960ebe1bc557b435da8687deef1302a08f030a33c5606fa6fb11b773b"}, + {file = "websockets-17.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:13cffaedde6e06183860fc92d88fe1d190abda73837fe4c14f5d4803c472f5dd"}, + {file = "websockets-17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ef2950896673a9809e8e5ab857525924fbcacfc98b858b0509ccf50d341f80f4"}, + {file = "websockets-17.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2d08ca4bf5a9cdb6fd81b86b51bd5cfb10527d3a1199888f2b02cc6c6a3d5f80"}, + {file = "websockets-17.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5e0c0ed37472c4729f3001066510a656486f08f149dda1ebfd3dfe4450c9756e"}, + {file = "websockets-17.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:62059d739af7fc035ed6e63c5390118a60a5e9aa5a5541cd0486a83e741842c7"}, + {file = "websockets-17.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:bc6f87d03e3691154673aaa8f08991723d6e3394c83ef2875c7a9049eac9595c"}, + {file = "websockets-17.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3d78d93846eddc990551c1c31914d90d71957ca3af468969826f7ed0d7d1d51c"}, + {file = "websockets-17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2a377c7132b1e69be7a50d7434d7f26f69e93c70166ebb6d8f8cabf802ac4506"}, + {file = "websockets-17.3-cp311-cp311-win32.whl", hash = "sha256:29b01520c39ef2f23c88b002f11e6ef4165675f6c92f5f3578fd587d69b5a5fc"}, + {file = "websockets-17.3-cp311-cp311-win_amd64.whl", hash = "sha256:d338813a440bf6d2f0df55140d3f3080284410b1ebfe9df50e3847bcdb1a17bb"}, + {file = "websockets-17.3-cp311-cp311-win_arm64.whl", hash = "sha256:6a8248b1ba8861e48e4c7ced3895a1264d2a3b617de72ec94172ce6e64bf16c7"}, + {file = "websockets-17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:41665c7ea01d3ae497a0c61fd22d3424230d18782a5fceb79f04db9ccb2042c9"}, + {file = "websockets-17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8f7469408740cf06a79b01605f2ae0b2acc8839c03fcbbf05da49baee12d3302"}, + {file = "websockets-17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d68da281ac208b6236ffa8beef23fe7c7c956d8bf09b421d447232fd94441b35"}, + {file = "websockets-17.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:078d1d41f45ee34eed5a4f3bbe7c588caecbdd6e12dfca73e8c1b0e803773793"}, + {file = "websockets-17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7f391838e8adcf1f2fe71561d9e5a9f74ea7ffa5052eb2db329aac677383136"}, + {file = "websockets-17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c15a165673bb3fb57facbf040edf5c0556d20ccd2e0ccc5f23ebb69fb715fdc1"}, + {file = "websockets-17.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28d1935c16782a1e3650c9baa79069b016fff739e292e4cb14bbb830d6930491"}, + {file = "websockets-17.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c534c9524a2acd0b97b7022074bec3784ac273c683cf9535087e437672ea8531"}, + {file = "websockets-17.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b459d29c8b82a420a98fa0ce8e9cf787ce0062d98cc90604ef9574fb3cc26ff8"}, + {file = "websockets-17.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53209dffd91b0aefed9ea630fa6ad5c2944d6210ba3a8a3a8664dc1575289b14"}, + {file = "websockets-17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9662a9fe457820c60365b3209e03cbd8e4a817195d55de65a80fa680b4bea136"}, + {file = "websockets-17.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5670a762c7ae388bf8aa04199fd436180a4d28318dca9c1eae7b38604676dbfd"}, + {file = "websockets-17.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ed59cb4ee258674a6ed9b8515e42079db02cb3be94f133fdd3f200183a40c49d"}, + {file = "websockets-17.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0f78bb2e82fe88912c9d7b25c98e57a9b08655579af89f164fd4c0657a075d62"}, + {file = "websockets-17.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:71250e010eede33c1d638e2c7a98288520d67f782fd5eceecaeca3129233a09a"}, + {file = "websockets-17.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:729989759feb81ff6493879a0d60a0bd7d2eee41001bbec0381a080318439382"}, + {file = "websockets-17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90d8c4fa5ba293cf92de351fbc292cdaadb1a1d58d031a1a97ff9b8fb6250ec7"}, + {file = "websockets-17.3-cp312-cp312-win32.whl", hash = "sha256:0824e6ecee838cc28e6481062574c7fde49f9a2cedfacd36f0e91cb4b37fd6fe"}, + {file = "websockets-17.3-cp312-cp312-win_amd64.whl", hash = "sha256:6644eecd99c4e8cdf72e646e17b391bb70b054f44becde37dcdc48cbec7e1527"}, + {file = "websockets-17.3-cp312-cp312-win_arm64.whl", hash = "sha256:4a9133dbaedc6f1af358127275676171d9a1bb3e61f47ee62b2ff9acb78af3d0"}, + {file = "websockets-17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:52a2ca6f401fb49a8d5d9e0e48ade5f58f1bdda33132907b0e4e3b702628fa4e"}, + {file = "websockets-17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:80578e91764bfca297ddca322c00a3136d993b829e9839e7fa378a19c1fcc25d"}, + {file = "websockets-17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fe7a168d805c8a7bf1c2de55b7ab2bc958586bfd88d0e87235a92cac66b590a7"}, + {file = "websockets-17.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:54fcd24944e6d127d198c28dc363967b7131ff0c97833962c2ef2048018de02d"}, + {file = "websockets-17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de2c12555bf05546f3ee75733f2c81de70aab7f530e86d0d2162b0ddcb3c39ae"}, + {file = "websockets-17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c95d25b5382ee0e6191ecafdeabefdc1f7bff8dfee29239b9b79e760ef3b04bd"}, + {file = "websockets-17.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8f244dfee05ea29b8064701880865eb1684f3a704a5665bd10b64694444dbbd4"}, + {file = "websockets-17.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e99440ef446c7833690c1754e23b0188d440f3cb89812b0764c697a7880cb33"}, + {file = "websockets-17.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:26838e2d95d7004c52e8c7b6b51676c872093a3b55a497824f899116a15a8a55"}, + {file = "websockets-17.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:508775efa11d26aec186d10154359f52276e2baf9ca201b52356330883276dfd"}, + {file = "websockets-17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2789d203babb6ceb2770aadc2394c1f99263ec37fba57a93064f9a8ad899e8ff"}, + {file = "websockets-17.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:cfef7f7aa434a1e632e0ebda14b39f4f1d7975e0fce5c77ea7b43156fabf9bd8"}, + {file = "websockets-17.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:39f532fa0d9026de4b9315a175998646112776baad54d55348435c11b05d034e"}, + {file = "websockets-17.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:85d6daa3a7ca404570ae130babbeb347ef9d3faca5300fb13cfa4bb48c3d30d1"}, + {file = "websockets-17.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ce39283ef08b0f28478e920d4d89a1fc169176e568e8a6a821332673145522eb"}, + {file = "websockets-17.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:23046098ebf844753cfd211033624a8247520e5bf2194c24cb1a8c222917acff"}, + {file = "websockets-17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:724224d4c457207271239114bf43afafc3eca4a1b4661ccdd428d11869a03bbd"}, + {file = "websockets-17.3-cp313-cp313-win32.whl", hash = "sha256:5af7070d80d278832cbca9c3c3dd44b30a26dcfa3ae1e0f6f19ff34edc27b689"}, + {file = "websockets-17.3-cp313-cp313-win_amd64.whl", hash = "sha256:66027e401c70d0cf55021b91b8a7ec2cd5810e20602f945b4fe6ae8b37ed232b"}, + {file = "websockets-17.3-cp313-cp313-win_arm64.whl", hash = "sha256:6b14914e67044724c31a9360c7b89e9d070117c630746eefdc1620d967545acd"}, + {file = "websockets-17.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:bc8fd230ef013951900c1fda100eebe4a4b1b407dde2a44186995042dda2c628"}, + {file = "websockets-17.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cdb8e4b0f6a41b857c74c03116e8ffbef59ef8e533e934b532843178990d01d5"}, + {file = "websockets-17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:018fab26140ce221c3853454a608e4e7004f4f10641cb97819dcd5b917bb2e99"}, + {file = "websockets-17.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dd0f3037d501178a761fd2814370aca547f79e99a200a61a1f6e7b79ec3d78b9"}, + {file = "websockets-17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8dbb3862aa649de95660b6337d97101977f160b5b354d07b82fe309874d2c8bc"}, + {file = "websockets-17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af37a1e6b6bd506b597d5fb4cf111fb527d1b2223608a5d441d4738827cfd166"}, + {file = "websockets-17.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:740bba4223fa5216d4d6c5dcd29e697f37229b8a61b69e1a50527b0ea993ab2c"}, + {file = "websockets-17.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4170556762c6fd2913d99dfdd8dbb4c927584899df7facb1034627e355263c45"}, + {file = "websockets-17.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d355b22270be8062360b9df2ee0b10277590789a1059257916fb9e256a0cff80"}, + {file = "websockets-17.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4aa85b34c21cf97b3470793d0bf0763d32bfd1e67b83067a0a3ba8dc3474dacf"}, + {file = "websockets-17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a0e2e7c2c1a4f25db7b6596738d810c10732e77f852247300a1bc49e364d6dc9"}, + {file = "websockets-17.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7fa69212ea60ea24e2d8aff23fe102b1f87f1b27a320f92d2e7b3468997a496a"}, + {file = "websockets-17.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:80a6e37e235780f189558ab804f3cbc01c18f5c578995739b49613d544d34ebf"}, + {file = "websockets-17.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:628efe13630ac3397e7d1278b6be6f22f37a86d638a324334b4cd43c1daf9928"}, + {file = "websockets-17.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:443fa28652682d2a3ec3eca49af1b87cb2523561257683e62e03a98967b22138"}, + {file = "websockets-17.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6705af9d1005127302c04d9f1533ba56e289336c465923ef878430e99295a0b9"}, + {file = "websockets-17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9bde5d82d7ac4859e0cb3a35e148f537a8d4e56aa43f42582b5f263982f095ae"}, + {file = "websockets-17.3-cp314-cp314-win32.whl", hash = "sha256:be7be57097a65a8fab3d0f0a10d12931deb626a7d8082eb732b1ae41f3a202e3"}, + {file = "websockets-17.3-cp314-cp314-win_amd64.whl", hash = "sha256:2b06213c8e1ce93b18d0476d7a5e647436c8f23d099888c9065e930e80f4bed5"}, + {file = "websockets-17.3-cp314-cp314-win_arm64.whl", hash = "sha256:6b7b540bb321f7f0a7b8f5d107f941bb361e3a154c0b594dae0394997b91b743"}, + {file = "websockets-17.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:c9d06edfd974e918b40e0662a7e76dac13125529ee5ec9fbe42dc0d6faa209c3"}, + {file = "websockets-17.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0a0bcf66289b9d017c175562c35c5304b5cc3ed56aded994ae30b0a9654ba405"}, + {file = "websockets-17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f3ed747b7651668131029c720fbc32c7ee2d129bdad200e6c8f4037b20f7913a"}, + {file = "websockets-17.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5868a02c4f6d698ddd19b4c9742748c03dec045289915806845189d96bc3d314"}, + {file = "websockets-17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa87ea2ad64e59fe5622c1dc5716106b6d6e7e4b26b848cb5f5710a75c292ef5"}, + {file = "websockets-17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65e937576141ac50fac32fa9736dbb3e0e4a7769f91be004011a09216c6ab05f"}, + {file = "websockets-17.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:35381b575f70a08d4241e73c0cde4672bb9b2a9aae8767ca6b7fb61305a052c8"}, + {file = "websockets-17.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5142798871a248b422f353e0684d51dfa119de3403a6fa0904b7cb806d9790ce"}, + {file = "websockets-17.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e164c4a6868d11988c7f2386e3278ccff21c665cd7d08279b62709dd37a3167c"}, + {file = "websockets-17.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10b24e836ee0233ca078e1244f6ba40343a22127a9a736f46a7e7b18de2cc711"}, + {file = "websockets-17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:56a76556ab04ab54a675acce2cac8b4e74ea2cec558a7c9ed37e0367b04b5d50"}, + {file = "websockets-17.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:33221e49be79d7bfe6d12bc52ef9458b256ab74fa8ac5660acfa3065de1c7870"}, + {file = "websockets-17.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2fa79b9a48ae59744316c71a34cb9f76f30c93e117338522c5135cb7eac10351"}, + {file = "websockets-17.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f87f02afe05c1c246012e3de100a77cfbabd17db709fe93cf074271ab239b312"}, + {file = "websockets-17.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8135a549e27e9941a70e2d2156676ef2ffeff4b06e9d564dbaaae963482afdef"}, + {file = "websockets-17.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ace8dd1efa5f44c482d091aa38322e20e3ffe5adcdba6b67ad9f4e7da58f3f68"}, + {file = "websockets-17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2129c9a5fc05a230ce088745a790af073e582b584badc7b05db067ee326f56d7"}, + {file = "websockets-17.3-cp314-cp314t-win32.whl", hash = "sha256:af7c474f0d8f4d2a196370e85033873c7b063a0b5da9a4f143aa023e3bd25407"}, + {file = "websockets-17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0c63ad657695e8f1a48624754cb6b464118ac50d4c1d477c26461c98390f0a62"}, + {file = "websockets-17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:3c4f631dc8d00e470cde3ec0a35d4b58a817bea592411956f1cf4099dbb306b3"}, + {file = "websockets-17.3-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:f300f28dff74ff4148c21e6971e4de6ca0c34e0086a3d95c017c5174ebf85ca5"}, + {file = "websockets-17.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:32029441af953e980b9940f042b337fe4ad9a97fa8b17a4ed878ec67b5b89ef4"}, + {file = "websockets-17.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:09f007b2150eff015b2109f006e2ac2012d376f41486defa88ae78a776b00cff"}, + {file = "websockets-17.3-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bdc658c9474864893e0b6b94f1390bf6a912952df1870dd93bb7d1a902b5628"}, + {file = "websockets-17.3-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:15decc0fe293d9e3f924bd483f3d76f3d0ff26845112a470e3f8842fec2c82a1"}, + {file = "websockets-17.3-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:997617dd7856c5dad742bf6893b872319156dd43f47587541a59e4a852d52bbc"}, + {file = "websockets-17.3-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a004c1fe5a72c496f28c6c1d8c2dc5cfe3278d99478e18f8b838c70a912b366f"}, + {file = "websockets-17.3-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35fe35d20baa661cef4a12ee7bc42eaf2376d43ef9816ab29dd47ca957f7a73a"}, + {file = "websockets-17.3-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8b3bede7d583bc9f47d8693a29984882e59077db1a5238ad977fc70ae8169741"}, + {file = "websockets-17.3-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dafbb58a149d1ebd5767b3f064f911c5ee0897608005e51c3904b0ada2a8d99c"}, + {file = "websockets-17.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:80326bc571164bbbb6bead2e35d0c943b23b24fde629032197c58151529d24d8"}, + {file = "websockets-17.3-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:7d53fc4b90ef6e11de639dc18744d9be8cbd70ccb5ce80027aaffe7b1ab0215a"}, + {file = "websockets-17.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:fdd00937f15f5f93c8e53d8060a8c4c010431ab595a9b5b50ddd569a04603944"}, + {file = "websockets-17.3-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1dae5cd6c92d4440b15f20aa2987baa73c47ea14baa0fec8536a542e82e0ab33"}, + {file = "websockets-17.3-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:b225c656efa2dedecc81651d2290170af25887b7de4f38c5efc7c5ae2b9442ab"}, + {file = "websockets-17.3-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:bd645b5d2f810a181c44f4a71135e7c365b44f4be9ed3d1892635be13a857553"}, + {file = "websockets-17.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d2605a3b2af8d66f8b75312d223642c065ec773446c04566d9030ad5e2db13c9"}, + {file = "websockets-17.3-cp315-cp315-win32.whl", hash = "sha256:82e60c00cfb8493a5ba27cdf9b3695fdd153f666f73e11f18968f78ead1d8705"}, + {file = "websockets-17.3-cp315-cp315-win_amd64.whl", hash = "sha256:4d5f2492cb2192aab2d42e57f2caa2159b4fd20fde06f859ec13354f524b176c"}, + {file = "websockets-17.3-cp315-cp315-win_arm64.whl", hash = "sha256:55494475206113dbaf731dc8c5ffd5e1094175b753b7bbaad09fe3d7df61165c"}, + {file = "websockets-17.3-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:23cb6f6d64e3a389bb8ba150f947f9a6c8c0aca383e814ca3c5cffab738d5646"}, + {file = "websockets-17.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f27e7e8a85512b6a4b490d5756edde8e6772883a5800e1476172e44eda3f7d7c"}, + {file = "websockets-17.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:6342f79d6f50e680c8aa59e0615d745e867e51ffd59c77093be60b7c433067af"}, + {file = "websockets-17.3-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fc23e72bd7f8a8e540fed2244524d0a9aeb6ae3325f3d56f04a7a3debe5b04c2"}, + {file = "websockets-17.3-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:722ab747ff5dc28070b1a8c020dba6466f13f2ab56bc251ad301fefa2454aa1e"}, + {file = "websockets-17.3-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d20d6ea5aabbeba2e054d33e405192ef75dc12947bbd00ec97d23b14969d325"}, + {file = "websockets-17.3-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:90cc9973c51811918247a78d9ab2f54871296e4e37e69a98aba4d3c9acedbf62"}, + {file = "websockets-17.3-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63b87c93e28957e6dc2bb92d31727a863c9bf4a4daa080f0995fdcb0242c9166"}, + {file = "websockets-17.3-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f7431e71072962d3ed1768038200aa4f1e76d9c2d7bedd554d57a55faaf6644"}, + {file = "websockets-17.3-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:534ed583fd4323de45d196535a444cebd1af2105e6e4e0cdcbf8cea266a8747a"}, + {file = "websockets-17.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:8879b6a4040ee925c638548238ab7fe612cf5fa8752fe847346da0da8be81c3f"}, + {file = "websockets-17.3-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:58e5d1e935a90878a935c7d2737a25c71cf4f1c13b6e6bef454ab722c4f338bb"}, + {file = "websockets-17.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:0a657b51e4a5f4ac577c11e706ff9a884ea5aaf3e2666bae283a251c63e66acb"}, + {file = "websockets-17.3-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:411aa0fb234f42a1444dea1e177890ddfc577b36ffdfafec25cb0d2c818e1af8"}, + {file = "websockets-17.3-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:623d4b44f64efdf235b1e8dce266057073d93cde6197e7d7684f20c297f67f30"}, + {file = "websockets-17.3-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:5d0c871efc696ef5eb8514df64b86ab28736db753ebb42a2101f3b562f4849c0"}, + {file = "websockets-17.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1db2604e6aedb7a2cb2f6e0575d6c9a852ccac8239703a930ac83b63309177a5"}, + {file = "websockets-17.3-cp315-cp315t-win32.whl", hash = "sha256:07246d4f0293ef0b87eff3d7dd88c1f03dd9f678fa39f6de87b2af8504e7e0de"}, + {file = "websockets-17.3-cp315-cp315t-win_amd64.whl", hash = "sha256:856bbb24f2ee6436af7609267e73309e4bb0a0f9982f79f1c2e01019279c6671"}, + {file = "websockets-17.3-cp315-cp315t-win_arm64.whl", hash = "sha256:a22c4956a4d858200dba83f0691bb692d77b0eeddfca80157820d20b85dea4f1"}, + {file = "websockets-17.3-pp311-pypy311_pp80-macosx_10_15_x86_64.whl", hash = "sha256:34c78052277053bcb09810ac8e9ba4029f9e3e277833fc18ec1c928a6246ac91"}, + {file = "websockets-17.3-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:ef07e0b6b879f699a16fc7a513b974246fdeaef44908a021130cf4afe6a1c89c"}, + {file = "websockets-17.3-pp311-pypy311_pp80-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99c5637b74e2805ee4de9e2a43c8b45f678e50abb1c2da9880f0372f6498ba2f"}, + {file = "websockets-17.3-pp311-pypy311_pp80-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c460aeb12c2bd1c204944d9e9199595bf9e505eb542018d304bf9e63cb60cad"}, + {file = "websockets-17.3-pp311-pypy311_pp80-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e1c174b366565e101f903f9d1cc96d5508f805759dd37d012743f1c73ec0d7b"}, + {file = "websockets-17.3-pp311-pypy311_pp80-win_amd64.whl", hash = "sha256:c4b3f2c7d84f303da873b634d0beef83b5e1137294c3de4e23102841dac0cf4a"}, + {file = "websockets-17.3-pp312-pypy312_pp80-macosx_10_15_x86_64.whl", hash = "sha256:0e65260a1b1d7b81651f47343d836de70b88b02358ae3658efa6382104ec37b1"}, + {file = "websockets-17.3-pp312-pypy312_pp80-macosx_11_0_arm64.whl", hash = "sha256:94c02b0702656f083c098bdbd93bce22a7aa7a2b34ba56d2472efb67dab7cf58"}, + {file = "websockets-17.3-pp312-pypy312_pp80-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ade76f8c84ade325bbed96806357fffd353fcc29a5166d6dc6d39381f6d9f76c"}, + {file = "websockets-17.3-pp312-pypy312_pp80-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9a215749c9a88ef2842e880215193e56ae60b856773c99702b078236bc6d4adb"}, + {file = "websockets-17.3-pp312-pypy312_pp80-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7b5d23ef144b32645d97bd99f382c4394c443395f45fe1576786838d26e308"}, + {file = "websockets-17.3-pp312-pypy312_pp80-win_amd64.whl", hash = "sha256:13eace1b315345ebac5b27821f5dcce24e5cf9976ab18f79e00a59bdd67e5d19"}, + {file = "websockets-17.3-py3-none-any.whl", hash = "sha256:dc2b495a92b3971966bf102e73589f950db3ec1ca6713265355ce7771d97b199"}, + {file = "websockets-17.3.tar.gz", hash = "sha256:1c7dd93a1c9f89297471493674b1fc1d9467cafd45374f2172213046bdb3a329"}, ] [[package]] name = "zipp" -version = "4.1.0" +version = "4.1.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.10" groups = ["main"] markers = "python_version < \"3.12\"" files = [ - {file = "zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f"}, - {file = "zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602"}, + {file = "zipp-4.1.2-py3-none-any.whl", hash = "sha256:b30cb6cd253122169c47de245ca24a86bac2a5d3802ba15bf7ba6414eb9e144d"}, + {file = "zipp-4.1.2.tar.gz", hash = "sha256:cf474b90b07ea932637e17c6b9479de2b2882900b0d311bdaf4f5c000a15a97f"}, ] [package.extras] @@ -3844,4 +4878,4 @@ type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "907371566c23fe8e3a8301b5b635461851b491d2d67683d2b6b72cb9dfee6987" +content-hash = "afecd54c466f9a1dd99de93156d633b600cdef993a82d4fe241bc2d98d818b72" diff --git a/pyproject.toml b/pyproject.toml index ca45b1f9d..3b96c514c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ [tool.poetry] name = "roc-validator" -version = "0.11.2" +version = "0.13.1" description = "A Python package to validate RO-Crates" authors = [ "Marco Enrico Piras ", @@ -26,7 +26,7 @@ license = "Apache-2.0" # Links to the GitHub repository and the project homepage/documentation repository = "https://github.com/crs4/rocrate-validator" homepage = "https://github.com/crs4/rocrate-validator" -documentation = "https://github.com/crs4/rocrate-validator" +documentation = "https://rocrate-validator.readthedocs.io/en/latest/" # Keywords and classifiers keywords = [ "RO-Crate", @@ -76,7 +76,7 @@ python = ">=3.10,<4.0" rdflib = ">=7.1,<8.0" pyshacl = ">=0.26" click = ">=8.2,<9.0" -rich = ">=13.9,<14.0" +rich = ">=13.9,<15.0" toml = ">=0.10.2,<1.0" rich-click = ">=1.8,<2.0" colorlog = ">=6.9,<7.0" @@ -101,10 +101,12 @@ pytest-xdist = "^3.6.1" [tool.poetry.group.docs.dependencies] sphinx = "^8.1.3" nbsphinx = "^0.9.5" +pypandoc-binary = "^1.17" sphinx-toolbox = "^3.8.1" myst-parser = "^4.0.0" -sphinx-rtd-theme = "^3.0.2" +shibuya = "==2026.7.12" sphinx-copybutton = "^0.5.2" +sphinx-autobuild = "==2024.10.3" [build-system] requires = ["poetry-core"] @@ -204,6 +206,10 @@ disable = [ # the underscore signals "internal API" within the package, not a hard # privacy boundary. Per-callsite suppressions would be noisy. "protected-access", # W0212 + # The crate readers and text report layout depend on each other's shared + # model helpers; Pylint reports this intentional package-level cycle at + # graph-build time, before file-level suppressions are considered. + "cyclic-import", # R0401 ] # Mypy configuration. @@ -214,6 +220,14 @@ disable = [ [tool.mypy] python_version = "3.10" # lowest supported version (see dependencies.python) files = ["rocrate_validator"] # the package; tests are not type-checked (yet) +exclude = [ + # Versioned profiles are loaded from directories rather than imported as + # Python packages, so repeated script basenames map to duplicate modules. + "rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py", + "rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py", + "rocrate_validator/profiles/ro-crate/1.2/should/2_root_data_entity_identifier.py", + "rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.py", +] # Surface configuration/annotation drift without being noisy. warn_unused_configs = true diff --git a/rocrate_validator/cli/commands/cache.py b/rocrate_validator/cli/commands/cache.py index 768add09b..c81624577 100644 --- a/rocrate_validator/cli/commands/cache.py +++ b/rocrate_validator/cli/commands/cache.py @@ -138,6 +138,7 @@ def cache_info(ctx, cache_path: Path | None = None): @click.pass_context def cache_list( ctx, + *, cache_path: Path | None = None, url_filter: str | None = None, sort_by: str = "created", @@ -304,6 +305,7 @@ def cache_reset(ctx, cache_path: Path | None = None, yes: bool = False): @click.pass_context def cache_warm( ctx, + *, cache_path: Path | None = None, profiles_path: Path | None = None, extra_profiles_path: Path | None = None, diff --git a/rocrate_validator/cli/commands/errors.py b/rocrate_validator/cli/commands/errors.py index 7d3c9f50e..955b2e0cb 100644 --- a/rocrate_validator/cli/commands/errors.py +++ b/rocrate_validator/cli/commands/errors.py @@ -12,19 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json import sys -import textwrap +import traceback from rich.console import Console +from rich.markup import escape +from rich.padding import Padding +from rich.panel import Panel +from rich.syntax import Syntax -from rocrate_validator.errors import InvalidProfilePath, ProfileNotFound, ProfilesDirectoryNotFound -from rocrate_validator.utils import log as logging +from rocrate_validator.errors import BadSyntaxError, InvalidProfilePath, ProfileNotFound, ProfilesDirectoryNotFound -# Create a logger for this module -logger = logging.getLogger(__name__) - -def handle_error(e: Exception, console: Console) -> None: +def handle_error(e: Exception, console: Console, *, debug: bool = False) -> None: error_message = "" if isinstance(e, ProfilesDirectoryNotFound): error_message = f""" @@ -45,19 +46,67 @@ def handle_error(e: Exception, console: Console) -> None: To see the available profiles, run: [cyan bold]rocrate-validator profiles list[/cyan bold] """ - else: - error_message = f"\n\n[bold][[red]FAILED[/red]] Unexpected error: {e} !!![/bold]\n" - if logger.isEnabledFor(logging.DEBUG): - console.print_exception() - console.print( - textwrap.indent( - "This error may be due to a bug.\n" - "Please report it to the issue tracker " - "along with the following stack trace:\n", - " " * 9, - ) + elif isinstance(e, json.JSONDecodeError): + source_line = e.doc.split("\n")[e.lineno - 1] + error_message = ( + "The RO-Crate metadata is not valid JSON.\n" + f"Reason: line {e.lineno}, column {e.colno}: [red]{escape(e.msg)}[/red]\n" + f"{escape(source_line)}\n" + f"{' ' * (e.colno - 1)}[red]^[/red]\n" + "Correct the JSON syntax before validating the RO-Crate. " + "Profile auto-detection cannot read invalid JSON." + ) + elif isinstance(e, BadSyntaxError): + location = _format_syntax_error_location(e) + error_message = ( + "The validation profile could not be parsed.\n" + f"Path: {escape(e.path)}\n" + f"Reason: {location}[red]{escape(_summarize_syntax_error(e.message))}[/red]" ) - console.print_exception() + else: + error_message = f"Unexpected error: {escape(str(e))}" console.print(f"\n\n[bold][[red]ERROR[/red]] {error_message}[/bold]\n", style="white") + if debug: + _print_debug_traceback(e, console) sys.exit(2) + + +def _summarize_syntax_error(message: str) -> str: + """Return the useful part of a parser error without its source dump.""" + lines = [line.strip() for line in message.splitlines() if line.strip()] + for line in lines: + if line.startswith("Bad syntax"): + return line.partition(" at ^ in:")[0] + return lines[0] if lines else "Unknown syntax error" + + +def _format_syntax_error_location(error: BadSyntaxError) -> str: + """Format the parser location when the underlying parser provided one.""" + location = [] + if error.line is not None: + location.append(f"line {error.line}") + if error.character is not None: + location.append(f"character {error.character}") + return f"[orange1]{escape(', '.join(location))}[/orange1] --> " if location else "" + + +def _print_debug_traceback(error: Exception, console: Console) -> None: + """Render a safe, readable traceback for exceptions from third-party parsers.""" + traceback_text = "".join(traceback.format_exception(type(error), error, error.__traceback__)).rstrip() + if console.no_color or not getattr(console, "interactive", True): + console.print(f"Debug traceback:\n{traceback_text}", markup=False) + return + + console.print( + Padding( + Panel( + Syntax(traceback_text, "pytb", theme="ansi_dark", line_numbers=False, word_wrap=True), + title="[bold yellow]Debug traceback[/bold yellow]", + title_align="left", + border_style="yellow", + padding=(1, 1, 1, 1), + ), + (1, 1, 0, 1), + ) + ) diff --git a/rocrate_validator/cli/commands/profiles.py b/rocrate_validator/cli/commands/profiles.py index c2cc6a3db..08de1b747 100644 --- a/rocrate_validator/cli/commands/profiles.py +++ b/rocrate_validator/cli/commands/profiles.py @@ -17,7 +17,7 @@ from pathlib import Path from typing import Any -from rich.align import Align +from click.exceptions import Exit from rich.markdown import Markdown from rich.padding import Padding from rich.panel import Panel @@ -28,7 +28,14 @@ from rocrate_validator.cli.commands.errors import handle_error from rocrate_validator.cli.main import cli, click from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER -from rocrate_validator.models import LevelCollection, Profile, RequirementCheck, RequirementLevel, Severity +from rocrate_validator.models import ( + EffectiveRequirementCheck, + LevelCollection, + Profile, + RequirementCheckRelation, + RequirementLevel, + Severity, +) from rocrate_validator.utils import log as logging from rocrate_validator.utils.io_helpers.colors import get_severity_color from rocrate_validator.utils.io_helpers.output.text.layout.report import get_app_header_rule @@ -156,6 +163,62 @@ def list_profiles(ctx, no_paging: bool = False): # , profiles_path: Path = DEFA handle_error(e, console) +@profiles.command("check") +@click.argument("profile-identifier", type=click.STRING, default=DEFAULT_PROFILE_IDENTIFIER, required=True) +@click.option( + "--no-paging", is_flag=True, help="Disable paging", default=False, show_default=True, hidden=sys.platform == "win32" +) +@click.pass_context +def check_profile(ctx, profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER, no_paging: bool = False): + """Check the consistency of a profile and its inherited profiles.""" + console = ctx.obj["console"] + pager = ctx.obj["pager"] + interactive = ctx.obj["interactive"] + enable_pager = not no_paging and interactive and sys.platform != "win32" + failed = False + + try: + profile = services.get_profile( + profile_identifier, + profiles_path=ctx.obj["profiles_path"], + extra_profiles_path=ctx.obj["extra_profiles_path"], + ) + profiles = [*profile.inherited_profiles, profile] + results = [ + (checked_profile, result) for checked_profile in profiles for result in checked_profile.validate_checks() + ] + + table = Table( + title=f" Profile checks: {profile.identifier}", + title_style="italic bold cyan", + title_justify="left", + header_style="bold cyan", + border_style="bright_black", + ) + table.add_column("Profile", style="magenta bold") + table.add_column("Check", style="cyan") + table.add_column("Status", justify="center") + table.add_column("Message") + table.add_column("Details") + + for checked_profile, result in results: + status = "[green]PASS[/green]" if result.passed else "[red]FAIL[/red]" + failed |= not result.passed + details = ", ".join(f"{key}={value}" for key, value in result.details.items()) + table.add_row(checked_profile.identifier, result.check_id, status, result.message, details) + + with console.pager(pager=pager, styles=not console.no_color) if enable_pager else console: + console.print(get_app_header_rule()) + console.print(Padding(table, (0, 1))) + + except SystemExit: + raise + except Exception as e: + handle_error(e, console) + if failed: + raise Exit(1) + + @profiles.command("describe") @click.option( "-v", @@ -173,6 +236,7 @@ def list_profiles(ctx, no_paging: bool = False): # , profiles_path: Path = DEFA @click.pass_context def describe_profile( ctx, + *, profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER, check_identifier: str | None = None, profiles_path: Path = DEFAULT_PROFILES_PATH, @@ -186,10 +250,11 @@ def describe_profile( \b The check identifier accepts either form: * relative: . (e.g. "1.2") - * full: _. (e.g. "ro-crate-1.1_1.2") + * full: _. (e.g. "ro-crate-1.3_1.2") With -v on a single check, the source code of the check is shown. """ + # Get the console console = ctx.obj["console"] pager = ctx.obj["pager"] @@ -210,14 +275,14 @@ def describe_profile( # Single-check view if check_identifier: - check = __resolve_check__(profile, check_identifier) + effective_check = __resolve_check__(profile, check_identifier) with console.pager(pager=pager, styles=not console.no_color) if enable_pager else console: console.print(get_app_header_rule()) - __describe_check__(console, profile, check, verbose=verbose) + __describe_check__(console, profile, effective_check, verbose=verbose) return # Set the subheader title - subheader_title = f"[bold][cyan]Profile:[/cyan] [magenta italic]{profile.identifier}[/magenta italic][/bold]" + subheader_title = f"[bold][cyan]Profile:[/cyan] [italic magenta]{profile.identifier}[/italic magenta][/bold]" # Set the subheader content subheader_content = f"[bold cyan]Version:[/bold cyan] [italic green]{profile.version}[/italic green]\n" @@ -328,7 +393,7 @@ def __compacted_describe_profile__(profile): # Define columns table.add_column("#", style="cyan bold", justify="right") - table.add_column("Name", style="magenta bold", justify="left") + table.add_column("Name", style="hot_pink bold", justify="left") table.add_column("Description", style="white italic") table.add_column("# REQUIRED", style=__requirement_level_style__(LevelCollection.REQUIRED), justify="center") table.add_column("# RECOMMENDED", style=__requirement_level_style__(LevelCollection.RECOMMENDED), justify="center") @@ -346,24 +411,25 @@ def __verbose_describe_profile__(profile): table_rows = [] levels_list = set() count_checks = 0 - for requirement in profile.requirements: - # skip hidden requirements - if requirement.hidden: + for effective_check in profile.get_effective_requirement_checks(): + check = effective_check.check + if check.requirement.hidden: continue - # add the requirement to the list - for check in requirement.get_checks(): - color = get_severity_color(check.severity) - level_info = f"[{color}]{check.severity.name}[/{color}]" - levels_list.add(level_info) - override = __format_overrides__(check.overrides, label="overrides") if check.overrides else None - - description_table = Table(show_header=False, show_footer=False, show_lines=False, show_edge=False) - if override: - description_table.add_row(Align(Padding(override, (0, 0, 1, 0)), align="right")) - description_table.add_row(Markdown(check.description.strip())) - - table_rows.append((check.identifier, check.name, description_table, level_info)) - count_checks += 1 + color = get_severity_color(check.severity) + level_info = f"[{color}]{check.severity.name}[/{color}]" + levels_list.add(level_info) + relation = __format_check_relation__(effective_check) + table_rows.append( + ( + effective_check.identifier, + check.name, + relation, + effective_check.source_identifier, + Markdown(check.description.strip()), + level_info, + ) + ) + count_checks += 1 table = Table( show_header=True, @@ -374,12 +440,18 @@ def __verbose_describe_profile__(profile): show_footer=False, show_lines=True, caption_style="italic bold", - caption=f"[cyan](*)[/cyan] number of checks by severity level: {', '.join(levels_list)}", + caption=( + f"[cyan](*)[/cyan] number of checks by severity level: {', '.join(levels_list)}\n" + "[cyan](†)[/cyan] Effective ID is the identity exposed by this profile; " + "Source ID identifies the check implementation" + ), ) # Define columns - table.add_column("Identifier", style="cyan bold", justify="right") - table.add_column("Name", style="magenta bold", justify="left") + table.add_column("Effective ID (†)", style="cyan bold", justify="right") + table.add_column("Name", style="hot_pink bold", justify="left") + table.add_column("Relation", justify="left") + table.add_column("Source ID (†)", style="magenta italic", justify="right") table.add_column("Description", style="white italic") table.add_column("Severity (*)", style="bold", justify="center") @@ -392,94 +464,86 @@ def __verbose_describe_profile__(profile): _CHECK_ID_RE = re.compile(r"^(?P\d+)\.(?P\d+)$") -def __resolve_check__(profile: Profile, check_identifier: str) -> RequirementCheck: +def __resolve_check__(profile: Profile, check_identifier: str) -> EffectiveRequirementCheck: """ - Resolve a check identifier to a RequirementCheck instance. + Resolve a check identifier to its effective profile view. Accepts either the relative form ``.`` or the full form ``_.``. """ raw = check_identifier.strip() - relative = raw - prefix = f"{profile.identifier}_" + effective_checks = tuple( + item for item in profile.get_effective_requirement_checks() if not item.check.requirement.hidden + ) if "_" in raw: - if not raw.startswith(prefix): + match = next((item for item in effective_checks if item.identifier == raw), None) + if match is None: raise click.BadParameter( - f"Check identifier '{raw}' does not belong to profile '{profile.identifier}'.", + f"Check identifier '{raw}' is not part of effective profile '{profile.identifier}'.", param_hint="CHECK_IDENTIFIER", ) - relative = raw[len(prefix) :] + return match - match = _CHECK_ID_RE.match(relative) - if not match: + if not _CHECK_ID_RE.match(raw): raise click.BadParameter( f"Invalid check identifier '{check_identifier}'. " f"Expected '.' (e.g. '1.2') or " f"'_.' (e.g. '{profile.identifier}_1.2').", param_hint="CHECK_IDENTIFIER", ) - req_number = int(match.group("req")) - check_number = int(match.group("check")) - - requirement = next( - (r for r in profile.requirements if not r.hidden and r.order_number == req_number), - None, - ) - if requirement is None: + matches = [item for item in effective_checks if item.identifier.rsplit("_", maxsplit=1)[-1] == raw] + if not matches: raise click.BadParameter( - f"No requirement #{req_number} in profile '{profile.identifier}'. " - f"Run `rocrate-validator profiles describe {profile.identifier}` to list requirements.", + f"No effective check '{raw}' in profile '{profile.identifier}'. " + f"Run `rocrate-validator profiles describe {profile.identifier} -v` to list checks.", param_hint="CHECK_IDENTIFIER", ) - check = next( - (c for c in requirement.get_checks() if c.order_number == check_number), - None, - ) - if check is None: + if len(matches) > 1: raise click.BadParameter( - f"No check #{check_number} in requirement #{req_number} of profile " - f"'{profile.identifier}'. Run `rocrate-validator profiles describe " - f"{profile.identifier} -v` to list checks.", + f"Relative check identifier '{raw}' is ambiguous in effective profile '{profile.identifier}'. " + "Use one of the full effective identifiers shown by the verbose profile description.", param_hint="CHECK_IDENTIFIER", ) - return check + return matches[0] -def __format_overrides__(checks: list, label: str) -> str: - """ - Format an "overrides" / "overridden by" Rich-styled string for a list of checks. - """ - parts = [] - for co in checks: - severity_color = get_severity_color(co.severity) - parts.append( - f"[bold][magenta]{co.requirement.profile.identifier}[/magenta] " - f"[{severity_color}]{co.relative_identifier}[/{severity_color}][/bold]" +def __format_check_relation__(effective_check: EffectiveRequirementCheck) -> str: + """Format the provenance relation of an effective check.""" + if effective_check.relation == RequirementCheckRelation.DEFINED_LOCALLY: + return "[bold yellow]Defined locally[/bold yellow]" + if effective_check.relation == RequirementCheckRelation.INHERITED: + return ( + f"[bold cyan]Inherited[/bold cyan] from " + f"[italic magenta]{effective_check.source_profile.identifier}[/italic magenta]" ) - return f"[bold red]{label}:[/bold red] " + ", ".join(parts) + replaced = ", ".join(check.identifier for check in effective_check.replaces) + return f"[bold orange1]Replaces[/bold orange1] [italic magenta]{replaced}[/italic magenta]" -def __describe_check__(console, profile: Profile, check: RequirementCheck, verbose: bool = False) -> None: +def __describe_check__( + console, profile: Profile, effective_check: EffectiveRequirementCheck, verbose: bool = False +) -> None: """ Render a single requirement check. """ + check = effective_check.check severity_color = get_severity_color(check.severity) requirement = check.requirement header = ( f"[bold cyan]Profile:[/bold cyan] " f"[italic magenta]{profile.identifier}[/italic magenta]\n" - f"[bold cyan]Identifier:[/bold cyan] " - f"[italic green]{check.identifier}[/italic green]\n" - f"[bold cyan]Name:[/bold cyan] [italic]{check.name}[/italic]\n" + f"[bold cyan]Effective ID:[/bold cyan] " + f"[italic green]{effective_check.identifier}[/italic green]\n" + f"[bold cyan]Name:[/bold cyan] [italic hot_pink]{check.name}[/italic hot_pink]\n" f"[bold cyan]Severity:[/bold cyan] " f"[bold {severity_color}]{check.severity.name}[/bold {severity_color}]\n" f"[bold cyan]Requirement:[/bold cyan] " - f"[italic]#{requirement.order_number} β€” {requirement.name}[/italic]" + f"[italic hot_pink]#{requirement.order_number} β€” {requirement.name}[/italic hot_pink]" ) if requirement.path: header += f"\n[bold cyan]Source file:[/bold cyan] [italic green]{shorten_path(requirement.path)}[/italic green]" - title = f"[bold][cyan]Check:[/cyan] [magenta italic]{check.identifier}[/magenta italic][/bold]" + title = f"[bold][cyan]Check:[/cyan] [magenta italic]{effective_check.identifier}[/magenta italic][/bold]" console.print( Padding( Panel(header, title=title, padding=(1, 1, 1, 1), title_align="left", border_style="cyan"), @@ -496,34 +560,25 @@ def __describe_check__(console, profile: Profile, check: RequirementCheck, verbo ) console.print(Padding(description_panel, (1, 1, 0, 1))) - if check.overrides: - overrides_text = __format_overrides__(check.overrides, label="overrides") - console.print( - Padding( - Panel( - overrides_text, - title="[bold cyan]Overrides[/bold cyan]", - title_align="left", - border_style="bright_black", - padding=(1, 1, 1, 1), - ), - (1, 1, 0, 1), - ) - ) - if check.overridden_by: - overridden_text = __format_overrides__(check.overridden_by, label="overridden by") - console.print( - Padding( - Panel( - overridden_text, - title="[bold cyan]Overridden by[/bold cyan]", - title_align="left", - border_style="bright_black", - padding=(1, 1, 1, 1), - ), - (1, 1, 0, 1), - ) + provenance = ( + f"[bold cyan]Relation:[/bold cyan] {__format_check_relation__(effective_check)}\n" + f"[bold cyan]Effective ID:[/bold cyan] [italic cyan]{effective_check.identifier}[/italic cyan]\n" + f"[bold cyan]Source ID:[/bold cyan] [italic magenta]{effective_check.source_identifier}[/italic magenta]\n" + f"[bold cyan]Source profile:[/bold cyan] " + f"[italic magenta]{effective_check.source_profile.identifier}[/italic magenta]" + ) + console.print( + Padding( + Panel( + provenance, + title="[bold cyan]Provenance[/bold cyan]", + title_align="left", + border_style="bright_black", + padding=(1, 1, 1, 1), + ), + (1, 1, 0, 1), ) + ) if verbose: snippet = check.get_source_snippet() diff --git a/rocrate_validator/cli/commands/validate.py b/rocrate_validator/cli/commands/validate.py index 5c759ed03..113d2c7fd 100644 --- a/rocrate_validator/cli/commands/validate.py +++ b/rocrate_validator/cli/commands/validate.py @@ -19,8 +19,11 @@ from pathlib import Path import rich_click as click +from rich.markup import escape from rich.padding import Padding from rich.rule import Rule +from rich.table import Table +from rich.text import Text from rocrate_validator import constants, services from rocrate_validator.cli.commands.errors import handle_error @@ -29,10 +32,12 @@ from rocrate_validator.errors import ROCrateInvalidURIError from rocrate_validator.models import Severity, ValidationResult, ValidationSettings from rocrate_validator.utils import log as logging +from rocrate_validator.utils.io_helpers.colors import get_severity_color from rocrate_validator.utils.io_helpers.input import get_single_char, multiple_choice from rocrate_validator.utils.io_helpers.output.console import Console from rocrate_validator.utils.io_helpers.output.json import JSONOutputFormatter from rocrate_validator.utils.io_helpers.output.text import TextOutputFormatter +from rocrate_validator.utils.io_helpers.output.text.formatters import ValidationExtraChecksTextOutputFormatter from rocrate_validator.utils.io_helpers.output.text.layout.report import LiveTextProgressLayout, get_app_header_rule from rocrate_validator.utils.paths import get_profiles_path from rocrate_validator.utils.uri import validate_rocrate_uri @@ -75,6 +80,13 @@ def validate_uri(ctx, param, value): default=False, show_default=True, ) +@click.option( + "--packaging-mode", + type=click.Choice(["auto", "attached", "detached"], case_sensitive=False), + default="auto", + show_default=True, + help="Packaging context assumed from the input, or explicitly attached/detached", +) @click.option("-ff", "--fail-fast", is_flag=True, help="Fail fast validation mode", default=False, show_default=True) @click.option( "--creation-time", @@ -97,6 +109,13 @@ def validate_uri(ctx, param, value): default=False, show_default=True, ) +@click.option( + "--extra-checks", + is_flag=True, + help="Run all checks marked as extra_check; findings do not affect conformance", + default=False, + show_default=True, +) @click.option( "--profiles-path", type=click.Path(exists=True), @@ -137,6 +156,13 @@ def validate_uri(ctx, param, value): default=False, show_default=True, ) +@click.option( + "--no-profile-checks", + is_flag=True, + help="Skip consistency checks on validation profiles", + default=False, + show_default=True, +) @click.option( "-l", "--requirement-severity", @@ -164,7 +190,7 @@ def validate_uri(ctx, param, value): help=( "[bold yellow]Fully-Qualified-Check-IDs[/bold yellow] is a comma-separated list of checks to skip " "(may be specified multiple times). Each check must be specified by its " - "Fully Qualified Identifier, e.g., [bold cyan]ro-crate-1.1_12.1[/bold cyan]. The fully qualified " + "Fully Qualified Identifier, e.g., [bold cyan]ro-crate-1.3_11.1[/bold cyan]. The fully qualified " "check identifier has the format _., " "where is the position number of the Requirement in the profile, " "and is the position number of the RequirementCheck within that Requirement. " @@ -172,6 +198,13 @@ def validate_uri(ctx, param, value): "[bold orange1]rocrate-validator profiles describe -v[/bold orange1]" ), ) +@click.option( + "--show-skipped-checks", + is_flag=True, + help="List skipped checks and their reasons at the end of the validation", + default=False, + show_default=True, +) @click.option( "-v", "--verbose", @@ -250,18 +283,23 @@ def validate_uri(ctx, param, value): @click.pass_context def validate( ctx, + *, profiles_path: Path = DEFAULT_PROFILES_PATH, extra_profiles_path: Path | None = None, profile_identifier: tuple[str, ...] = (), metadata_only: bool = False, + packaging_mode: str = "auto", creation_time: bool = False, enforce_availability: bool = False, skip_availability_check: bool = False, + extra_checks: bool = False, no_auto_profile: bool = False, disable_profile_inheritance: bool = False, + no_profile_checks: bool = False, requirement_severity: str = Severity.REQUIRED.name, requirement_severity_only: bool = False, skip_checks: list[str] | None = None, + show_skipped_checks: bool = False, rocrate_uri: str | Path = ".", relative_root_path: Path | None = None, fail_fast: bool = False, @@ -330,6 +368,7 @@ def validate( "abort_on_first": fail_fast, "skip_checks": skip_checks_list, "metadata_only": metadata_only, + "packaging_mode": packaging_mode, "cache_max_age": cache_max_age if not no_cache else -1, "cache_path": cache_path, "offline": offline, @@ -340,6 +379,8 @@ def validate( "creation_time": creation_time, "enforce_availability": enforce_availability, "skip_availability_check": skip_availability_check, + "extra_checks": extra_checks, + "validate_profile_checks": not no_profile_checks, } # Print the application header @@ -353,11 +394,11 @@ def validate( # interactive selection, or fallback to the base `ro-crate` profile). profile_identifiers, autodetection = _resolve_profile_identifiers( console, - interactive, - no_auto_profile, - available_profiles, - list(profile_identifier), - validation_settings, + interactive=interactive, + no_auto_profile=no_auto_profile, + available_profiles=available_profiles, + profile_identifiers=list(profile_identifier), + validation_settings=validation_settings, ) # Validate the RO-Crate against the selected profiles @@ -365,7 +406,7 @@ def validate( results = {} for profile in profile_identifiers: # Duplicate settings for each profile and set the profile identifier - logger.info("\nValidating RO-Crate against profile: [bold cyan]%s[/bold cyan]", profile) + logger.info("\nValidating RO-Crate against profile: [bold magenta]%s[/bold magenta]", profile) profile_settings = validation_settings.copy() profile_settings["profile_identifier"] = profile logger.debug("Profile selected for validation: %s", profile) @@ -412,13 +453,21 @@ def validate( output_file=output_file, output_line_width=output_line_width, ) + _emit_skipped_checks_report( + results, + show_skipped_checks=show_skipped_checks, + output_format=output_format, + output_file=output_file, + output_line_width=output_line_width, + console=console, + ) # Exit with appropriate status code. # using ctx.exit seems to raise an Exception that gets caught below, # so we use sys.exit instead. sys.exit(0 if is_valid else 1) except Exception as e: - handle_error(e, console) + handle_error(e, console, debug=ctx.obj["debug"]) def _log_validation_inputs( @@ -490,8 +539,9 @@ def _parse_skip_checks(skip_checks: list[str] | None) -> list[str]: return skip_checks_list -def _resolve_profile_identifiers( +def _resolve_profile_identifiers( # noqa: C901, PLR0912 console: Console, + *, interactive: bool, no_auto_profile: bool, available_profiles: list, @@ -506,37 +556,83 @@ def _resolve_profile_identifiers( Returns the identifiers and whether they were auto-detected. """ autodetection = False + reason = None if not profile_identifiers: # Auto-detect the profile to use for validation (if not disabled) candidate_profiles = None + detection_result = None if not no_auto_profile: - candidate_profiles = services.detect_profiles(settings=validation_settings) + detection_result = services.detect_profiles_detailed(settings=validation_settings) + candidate_profiles = detection_result.profiles logger.debug("Candidate profiles: %s", candidate_profiles) else: logger.info("Auto-detection of the profiles to use for validation is disabled") - # Prompt the user when interactive and no single profile could be auto-detected - if interactive and ( - not candidate_profiles or len(candidate_profiles) == 0 or len(candidate_profiles) == len(available_profiles) - ): + if detection_result and detection_result.diagnostics: + diagnostic = detection_result.diagnostics[0] + reason = ( + f"`{escape(diagnostic.property or '')}` at {escape(diagnostic.location or '')} " + f"has value {escape(repr(diagnostic.value))}. {escape(diagnostic.message)}" + ) + for diagnostic in detection_result.diagnostics: + if diagnostic.code not in ("INVALID_CONFORMS_TO", "INVALID_REFERENCE"): + continue + console.print( + "\n [bold yellow]WARNING: Non-conforming metadata[/bold yellow]\n" + f" `{escape(diagnostic.property or '')}` at {escape(diagnostic.location or '')} " + f"has value {escape(repr(diagnostic.value))}.\n" + f" {escape(diagnostic.message)}" + ) + if candidate_profiles: + console.print( + " Profiles automatically selected for validation: " + + escape(", ".join(profile.identifier for profile in candidate_profiles)) + ) + + suggested_profile = detection_result.suggested_profile if detection_result else None + if interactive and not candidate_profiles and suggested_profile: + console.print( + " The string URI matches the available profile " + f"[magenta]{escape(suggested_profile.identifier)}[/magenta]. " + "Using it will not correct the metadata." + ) + confirmation = get_single_char( + console, + choices=["y", "n"], + message=("[bold]Use this profile for validation? ([magenta]y/n[/magenta]): [/bold]"), + ) + if confirmation == "y": + return [suggested_profile.identifier], True + elif not interactive and suggested_profile: + console.print( + " The string URI matches profile " + f"{escape(suggested_profile.identifier)}, but using it requires confirmation. " + "Use --profile-identifier to select it explicitly in non-interactive mode." + ) + + # Prompt the user only when no profile could be auto-detected or confirmed. + if interactive and not candidate_profiles: console.print( Padding( Rule( "[bold yellow]WARNING: [/bold yellow]" - "[bold]Unable to automatically detect the profile to use for validation[/bold]\n", + "[bold]Unable to automatically detect the profile to use for validation[/bold]" + + (f"\n{reason}" if reason else "") + + "\n", align="center", style="bold yellow", ), (2, 2, 0, 2), ) ) - selected_options = multiple_choice(console, available_profiles) - if selected_options is None or isinstance(selected_options, bool): - selected_options = [] - profile_identifiers = [available_profiles[int(o)].identifier for o in selected_options] - logger.debug("Profile selected: %s", selected_options) + if not profile_identifiers: + selected_options = multiple_choice(console, available_profiles) + if selected_options is None or isinstance(selected_options, bool): + selected_options = [] + profile_identifiers = [available_profiles[int(o)].identifier for o in selected_options] + logger.debug("Profile selected: %s", selected_options) console.print(Padding(Rule(style="bold yellow"), (1, 2))) - elif candidate_profiles and len(candidate_profiles) < len(available_profiles): + elif candidate_profiles: logger.debug("Profile identifier autodetected: %s", candidate_profiles[0].identifier) autodetection = True profile_identifiers = [_.identifier for _ in candidate_profiles] @@ -548,6 +644,8 @@ def _resolve_profile_identifiers( console.print("[bold]Auto-detection of the profiles to use for validation is disabled[/bold]") else: console.print("[bold]Unable to automatically detect the profile to use for validation[/bold]") + if reason: + console.print(f"{' ' * 11}{reason}") console.print(f"{' ' * 11}[bold]The base `ro-crate` profile will be used for validation[/bold]") profile_identifiers = ["ro-crate"] @@ -583,11 +681,17 @@ def _render_console_result( ) if verbose_choice == "y" or verbose: command_view.display_validation_result(result) + elif result.extra_checks: + console.print(ValidationExtraChecksTextOutputFormatter(result)) + elif result.extra_checks: + console.print(ValidationExtraChecksTextOutputFormatter(result)) return result result = services.validate(validation_settings) console.register_formatter(TextOutputFormatter()) console.print(result.statistics) + if result.extra_checks and (result.passed() or not verbose): + console.print(ValidationExtraChecksTextOutputFormatter(result)) if not result.passed() and verbose: out = Console(no_color=console.no_color, width=console.width, height=console.height) out.register_formatter(TextOutputFormatter()) @@ -630,6 +734,8 @@ def _render_file_or_collected_result( out = Console(color_system=None, width=output_line_width, height=31, file=f) out.register_formatter(TextOutputFormatter()) out.print(result.statistics) # Output the statistics first + if result.extra_checks and (result.passed() or not verbose): + out.print(ValidationExtraChecksTextOutputFormatter(result)) if not result.passed() and verbose: out.print(result) if interactive: @@ -653,7 +759,7 @@ def _emit_json_report( console.print( f"\n{' ' * 2}βœ… [bold]Validation [green]PASSED![/green]. " f"\n{' ' * 5}RO-Crate is valid according to the profile(s): " - f"[cyan]{', '.join(profile_identifiers)}[/cyan][/bold]" + f"[magenta]{', '.join(profile_identifiers)}[/magenta][/bold]" ) else: console.print(f"\n{' ' * 2}❌ [bold]Validation [red]FAILED![/red][/bold]") @@ -670,7 +776,57 @@ def _emit_json_report( with output_file.open("w", encoding="utf-8") if output_file else nullcontext(sys.stdout) as f: out = Console(width=output_line_width, file=f) out.register_formatter(JSONOutputFormatter()) - out.print(results) + # Disable word-wrap/cropping: Rich would otherwise insert literal line + # breaks into long string values (e.g. messages, URLs), producing + # invalid, unescaped control characters in the JSON output. + out.print(results, soft_wrap=True) if interactive and output_file: console.print("[bold]DONE![/bold]", end="\n\n") + + +def _print_skipped_checks(results: dict[str, ValidationResult], console: Console) -> None: + """Render the collected skipped-check details using the validation palette.""" + details = [detail for result in results.values() for detail in result.skipped_check_details] + if not details: + return + + table = Table( + title=f"Skipped Checks ({len(details)})", + title_style="bold yellow", + border_style="yellow", + header_style="bold yellow", + show_lines=False, + ) + table.add_column("Profile", style="cyan", no_wrap=True) + table.add_column("Check", no_wrap=True) + table.add_column("Category", style="yellow", no_wrap=True) + table.add_column("Reason") + for detail in details: + table.add_row( + Text(detail.to_dict()["profile"], style="cyan"), + Text(detail.to_dict()["identifier"], style=get_severity_color(detail.check.severity)), + Text(detail.category.value, style="yellow"), + Text(f"{detail.check.name}: {detail.message}"), + ) + console.print(Padding(table, (1, 2))) + + +def _emit_skipped_checks_report( + results: dict[str, ValidationResult], + *, + show_skipped_checks: bool, + output_format: str, + output_file: Path | None, + output_line_width: int | None, + console: Console, +) -> None: + """Write the optional skipped-check report without affecting JSON output.""" + if not show_skipped_checks or output_format == "json": + return + if output_file: + with output_file.open("a", encoding="utf-8") as output: + output_console = Console(color_system=None, width=output_line_width, file=output) + _print_skipped_checks(results, output_console) + else: + _print_skipped_checks(results, console) diff --git a/rocrate_validator/cli/main.py b/rocrate_validator/cli/main.py index ce57f6277..41b82ac5a 100644 --- a/rocrate_validator/cli/main.py +++ b/rocrate_validator/cli/main.py @@ -16,6 +16,7 @@ import rich_click as click +from rocrate_validator.cli.commands.errors import handle_error from rocrate_validator.cli.utils import running_in_jupyter from rocrate_validator.utils import log as logging from rocrate_validator.utils.io_helpers.output.console import Console @@ -68,6 +69,7 @@ def cli(ctx: click.Context, debug: bool, version: bool, disable_color: bool, no_ ctx.obj["console"] = console ctx.obj["pager"] = SystemPager() ctx.obj["interactive"] = interactive + ctx.obj["debug"] = debug try: # If the version flag is set, print the version and exit @@ -85,13 +87,7 @@ def cli(ctx: click.Context, debug: bool, version: bool, disable_color: bool, no_ else: logger.debug("Command invoked: %s", ctx.invoked_subcommand) except Exception as e: - console.print(f"\n\n[bold][[red]FAILED[/red]] Unexpected error: {e} !!![/bold]\n", style="white") - console.print("""This error may be due to a bug. - Please report it to the issue tracker - along with the following stack trace: - """) - console.print_exception() - sys.exit(2) + handle_error(e, console, debug=debug) if __name__ == "__main__": diff --git a/rocrate_validator/constants.py b/rocrate_validator/constants.py index 6d25b5dcd..cadc87461 100644 --- a/rocrate_validator/constants.py +++ b/rocrate_validator/constants.py @@ -17,6 +17,9 @@ from rdflib import Namespace +# Define the package distribution name +PACKAGE_NAME = "roc-validator" + # Define SHACL namespace SHACL_NS = "http://www.w3.org/ns/shacl#" @@ -39,7 +42,7 @@ ROCRATE_METADATA_FILE = "ro-crate-metadata.json" # Define the default profiles name -DEFAULT_PROFILE_IDENTIFIER = "ro-crate-1.1" +DEFAULT_PROFILE_IDENTIFIER = "ro-crate-1.3" # Define the default profiles path DEFAULT_PROFILES_PATH = "profiles" diff --git a/rocrate_validator/errors.py b/rocrate_validator/errors.py index 6d3da1133..41cf52b67 100644 --- a/rocrate_validator/errors.py +++ b/rocrate_validator/errors.py @@ -151,6 +151,32 @@ def __repr__(self): return f"DuplicateRequirementCheck({self._check_name!r}, {self._profile_name!r})" +class CheckDependencyError(ROCValidatorError): + """Raised when a check dependency cannot be resolved or ordered.""" + + def __init__(self, message: str, profile_name: str | None = None): + self._message = message + self._profile_name = profile_name + + @property + def message(self) -> str: + """The dependency configuration error message.""" + return self._message + + @property + def profile_name(self) -> str | None: + """The profile containing the invalid dependency.""" + return self._profile_name + + def __str__(self) -> str: + if self._profile_name: + return f"Invalid check dependency in profile {self._profile_name!r}: {self._message}" + return f"Invalid check dependency: {self._message}" + + def __repr__(self): + return f"CheckDependencyError({self._message!r}, {self._profile_name!r})" + + class InvalidSerializationFormat(ROCValidatorError): """Raised when an invalid serialization format is provided.""" @@ -172,10 +198,20 @@ def __repr__(self): class BadSyntaxError(ROCValidatorError): """Raised when a syntax error occurs.""" - def __init__(self, message, path: str = ".", code: int = -1): + def __init__( + self, + message, + path: str = ".", + code: int = -1, + *, + line: int | None = None, + character: int | None = None, + ): self._message = message self._path = path self._code = code + self._line = line + self._character = character @property def message(self) -> str: @@ -192,6 +228,16 @@ def code(self) -> int: """The error code.""" return self._code + @property + def line(self) -> int | None: + """The line where the syntax error occurred, when available.""" + return self._line + + @property + def character(self) -> int | None: + """The character where the syntax error occurred, when available.""" + return self._character + def __str__(self) -> str: return self._message @@ -229,6 +275,10 @@ def __repr__(self): return f"ValidationError({self._message!r}, {self._path!r})" +class ValidationExecutionError(ValidationError): + """Raised when validation cannot complete and no result can be trusted.""" + + class CheckValidationError(ValidationError): """Raised when a validation check fails.""" diff --git a/rocrate_validator/models/__init__.py b/rocrate_validator/models/__init__.py index eadc17bf8..a304f3a48 100644 --- a/rocrate_validator/models/__init__.py +++ b/rocrate_validator/models/__init__.py @@ -13,6 +13,7 @@ # limitations under the License. from rocrate_validator.models._logging import logger +from rocrate_validator.models.check_result import CheckResult, CheckResultValue, normalize_check_result from rocrate_validator.models.events import ( ProfileValidationEvent, RequirementCheckValidationEvent, @@ -20,18 +21,21 @@ ValidationEvent, ) from rocrate_validator.models.profile import Profile +from rocrate_validator.models.profile_check import ( + ProfileCheck, + ProfileCheckFailure, + ProfileCheckResult, + ProfileCheckSuite, +) +from rocrate_validator.models.profile_detection import ProfileDetectionDiagnostic, ProfileDetectionResult +from rocrate_validator.models.profile_provenance import EffectiveRequirementCheck, RequirementCheckRelation from rocrate_validator.models.requirement import ( Requirement, RequirementCheck, RequirementLoader, - SkipRequirementCheck, SourceSnippet, ) -from rocrate_validator.models.result import ( - CheckIssue, - CustomEncoder, - ValidationResult, -) +from rocrate_validator.models.result import CheckIssue, CustomEncoder, ValidationExtraCheck, ValidationResult from rocrate_validator.models.settings import ( DEFAULT_PROFILES_PATH, BaseTypes, @@ -42,6 +46,7 @@ RequirementLevel, Severity, ) +from rocrate_validator.models.skipped_check import SkipCategory, SkippedCheckDetail, SkipRequirementCheck from rocrate_validator.models.statistics import ( AggregatedValidationStatistics, ValidationStatistics, @@ -59,25 +64,39 @@ "AggregatedValidationStatistics", "BaseTypes", "CheckIssue", + "CheckResult", + "CheckResultValue", "CustomEncoder", + "EffectiveRequirementCheck", "LevelCollection", "Profile", + "ProfileCheck", + "ProfileCheckFailure", + "ProfileCheckResult", + "ProfileCheckSuite", + "ProfileDetectionDiagnostic", + "ProfileDetectionResult", "ProfileValidationEvent", "Requirement", "RequirementCheck", + "RequirementCheckRelation", "RequirementCheckValidationEvent", "RequirementLevel", "RequirementLoader", "RequirementValidationEvent", "Severity", + "SkipCategory", "SkipRequirementCheck", + "SkippedCheckDetail", "SourceSnippet", "ValidationContext", "ValidationEvent", + "ValidationExtraCheck", "ValidationResult", "ValidationSettings", "ValidationStatistics", "ValidationStatisticsListener", "Validator", "logger", + "normalize_check_result", ] diff --git a/rocrate_validator/models/check_result.py b/rocrate_validator/models/check_result.py new file mode 100644 index 000000000..cdcf43397 --- /dev/null +++ b/rocrate_validator/models/check_result.py @@ -0,0 +1,43 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from enum import Enum + + +class CheckResult(str, Enum): + """Normalized outcome of a requirement check.""" + + PASSED = "passed" + FAILED = "failed" + SKIPPED = "skipped" + + +CheckResultValue = bool | CheckResult | None + + +class DeferredRequirementCheck(Exception): + """Leave a check pending until its validation backend records an outcome.""" + + +def normalize_check_result(value: CheckResultValue) -> CheckResult: + """Convert legacy boolean and ``None`` outcomes to a check result.""" + if isinstance(value, CheckResult): + return value + if value is None: + return CheckResult.SKIPPED + if isinstance(value, bool): + return CheckResult.PASSED if value else CheckResult.FAILED + raise TypeError(f"Check results must be bool, CheckResult, or None, not {type(value).__name__}") diff --git a/rocrate_validator/models/events.py b/rocrate_validator/models/events.py index 6f06038c1..61d6b4dc2 100644 --- a/rocrate_validator/models/events.py +++ b/rocrate_validator/models/events.py @@ -19,6 +19,7 @@ from rocrate_validator.events import Event, EventType if TYPE_CHECKING: + from rocrate_validator.models.check_result import CheckResultValue from rocrate_validator.models.profile import Profile from rocrate_validator.models.requirement import Requirement, RequirementCheck from rocrate_validator.models.result import ValidationResult @@ -122,7 +123,7 @@ def __init__( self, event_type: EventType, requirement_check: RequirementCheck, - validation_result: bool | None = None, + validation_result: CheckResultValue = None, message: str | None = None, ): assert event_type in ( @@ -132,15 +133,42 @@ def __init__( super().__init__(event_type, message) self._requirement_check = requirement_check self._validation_result = validation_result + self._effective_identifier = requirement_check.identifier + self._effective_profile_identifier = requirement_check.requirement.profile.identifier @property def requirement_check(self) -> RequirementCheck: return self._requirement_check @property - def validation_result(self) -> bool | None: + def validation_result(self) -> CheckResultValue: return self._validation_result + @property + def effective_identifier(self) -> str: + """Identifier exposed for the active validation target.""" + return self._effective_identifier + + @property + def effective_profile_identifier(self) -> str: + """Profile exposed for the active validation target.""" + return self._effective_profile_identifier + + @property + def source_identifier(self) -> str: + """Identifier of the physically declared check.""" + return self.requirement_check.identifier + + @property + def source_profile_identifier(self) -> str: + """Profile that physically declares the check.""" + return self.requirement_check.requirement.profile.identifier + + def set_effective_identity(self, identifier: str, profile_identifier: str) -> None: + """Attach context-local reporting identity before dispatch.""" + self._effective_identifier = identifier + self._effective_profile_identifier = profile_identifier + def __str__(self) -> str: return f"RequirementCheckValidationEvent({self.event_type}, {self.requirement_check})" diff --git a/rocrate_validator/models/profile.py b/rocrate_validator/models/profile.py index 6d245fea2..459a52469 100644 --- a/rocrate_validator/models/profile.py +++ b/rocrate_validator/models/profile.py @@ -17,7 +17,7 @@ import re from functools import total_ordering from pathlib import Path -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, ClassVar, cast from rdflib import RDF, RDFS, Graph, Namespace, URIRef @@ -27,6 +27,7 @@ PROF_NS, PROFILE_SPECIFICATION_FILE, SCHEMA_ORG_NS, + VALIDATOR_NS, ) from rocrate_validator.errors import ( DuplicateRequirementCheck, @@ -36,6 +37,17 @@ ProfileSpecificationNotFound, ) from rocrate_validator.models._logging import logger +from rocrate_validator.models.profile_check import ( + NoRequirementCheckOverrides, + ProfileCheckFailure, + ProfileCheckResult, + ProfileCheckSuite, +) +from rocrate_validator.models.profile_provenance import ( + EffectiveRequirementCheck, + RequirementCheckRelation, +) +from rocrate_validator.models.requirement import RequirementLoader from rocrate_validator.models.severity import Severity from rocrate_validator.utils.collections import MapIndex, MultiIndexMap @@ -63,11 +75,13 @@ class Profile: MapIndex("token_path", unique=False), ], ) + __profile_check_results: ClassVar[dict[tuple[Path, Severity], tuple[ProfileCheckResult, ...]]] = {} def __init__( self, profiles_base_path: Path, profile_path: Path, + *, requirements: list[Requirement] | None = None, identifier: str | None = None, publicID: str | None = None, @@ -290,6 +304,19 @@ def is_transitive_profile_of(self) -> list[str]: """ return cast("list[str]", self.__get_specification_property__("isTransitiveProfileOf", PROF_NS, pop_first=False)) + @property + def is_rule_overlay_of(self) -> list[str]: + """ + Direct parent profiles whose validation rules are overlaid by this profile. + + This implementation relationship is deliberately separate from + ``prof:isProfileOf``: ordinary profile inheritance retains its source + identity, while overlay rules are reported in the target namespace. + Profile consistency checks require every value to resolve to a loaded + direct parent and reject ambiguous check identities across sources. + """ + return cast("list[str]", self.__get_specification_property__("isRuleOverlayOf", VALIDATOR_NS, pop_first=False)) + @property def parents(self) -> list[Profile]: """ @@ -398,15 +425,156 @@ def get_requirement(self, name: str) -> Requirement | None: return requirement return None - def get_requirement_check(self, check_name: str) -> RequirementCheck | None: + def get_requirement_checks( + self, + check_name: str, + severity: Severity | None = None, + ) -> list[RequirementCheck]: + """Return checks matching a name and, when provided, a severity.""" + return [ + check + for requirement in self.requirements + for check in requirement.get_checks() + if check.name == check_name and (severity is None or check.severity == severity) + ] + + def get_requirement_check( + self, + check_name: str, + severity: Severity | None = None, + ) -> RequirementCheck | None: """ - Get the requirement check with the given name + Get the check matching a name and optional severity. + + A check identity must be unique within a profile. Ambiguous lookups fail + instead of making override behavior depend on requirement load order. """ - for requirement in self.requirements: - check = requirement.get_check(check_name) - if check: - return check - return None + checks = self.get_requirement_checks(check_name, severity) + if len(checks) > 1: + identity = f"{check_name} [{severity.name}]" if severity else check_name + raise DuplicateRequirementCheck(identity, self.identifier) + return checks[0] if checks else None + + @staticmethod + def __check_identity__(check: RequirementCheck) -> tuple[str, Severity]: + return check.name, check.severity + + def effective_requirement_check(self, check: RequirementCheck) -> EffectiveRequirementCheck: + """ + Return the provenance and reporting identity of ``check`` in this profile. + + A check defined by this profile replaces direct-parent checks with the + same name and severity. Checks composed through ``isRuleOverlayOf`` are + reported in this profile's namespace; ordinary inherited checks retain + the effective identity exposed by their ancestor. + """ + source_profile = check.requirement.profile + if source_profile == self: + replaced_checks = tuple(check.overrides) + # relative_identifier is " ."; omit the severity in the effective ID. + relative_identifier = check.relative_identifier.split(" ", maxsplit=1)[-1] + overlay_replacements = tuple( + parent_check + for parent_check in replaced_checks + if parent_check.requirement.profile.uri in self.is_rule_overlay_of + ) + if len(overlay_replacements) == 1: + parent_check = overlay_replacements[0] + parent_identity = parent_check.requirement.profile.effective_requirement_check(parent_check) + relative_identifier = parent_identity.identifier.rsplit("_", maxsplit=1)[-1] + return EffectiveRequirementCheck( + check=check, + identifier=f"{self.identifier}_{relative_identifier}", + profile=self, + relation=( + RequirementCheckRelation.REPLACES if replaced_checks else RequirementCheckRelation.DEFINED_LOCALLY + ), + replaces=replaced_checks, + ) + + # Resolve through the nearest ancestor exposing this check, including + # its replacement identity. Each overlay then rebases that effective + # identity, while ordinary inheritance preserves it. + for inherited_profile in reversed(self.inherited_profiles): + if source_profile == inherited_profile or source_profile in inherited_profile.inherited_profiles: + inherited_identity = inherited_profile.effective_requirement_check(check) + is_overlay = inherited_profile.uri in self.is_rule_overlay_of + relative_identifier = inherited_identity.identifier.rsplit("_", maxsplit=1)[-1] + return EffectiveRequirementCheck( + check=check, + identifier=( + f"{self.identifier}_{relative_identifier}" if is_overlay else inherited_identity.identifier + ), + profile=self if is_overlay else inherited_identity.profile, + relation=RequirementCheckRelation.INHERITED, + ) + + return EffectiveRequirementCheck( + check=check, + identifier=check.identifier, + profile=source_profile, + relation=RequirementCheckRelation.INHERITED, + ) + + def get_effective_requirement_checks(self) -> tuple[EffectiveRequirementCheck, ...]: + """ + Return the checks that form this profile, including inherited checks. + + More-specific definitions shadow inherited checks with the same + ``(name, severity)`` identity. The returned objects keep source and + effective identities separate, so callers do not need a validation + context and never have to mutate the source model. + """ + effective_checks: list[EffectiveRequirementCheck] = [] + seen_identities: dict[tuple[str, Severity], tuple[RequirementCheck, int]] = {} + for source_profile in (self, *self.inherited_profiles): + for requirement in source_profile.requirements: + for check in requirement.get_checks(): + identity = self.__check_identity__(check) + previous = seen_identities.get(identity) + if previous is not None: + previous_check, previous_index = previous + previous_profile = previous_check.requirement.profile + # A transitive ancestor is shadowed by the nearer + # profile already selected. Unrelated parents form an + # ambiguous effective identity and must not be hidden. + if source_profile in previous_profile.inherited_profiles: + continue + if previous_profile in source_profile.inherited_profiles: + effective_checks[previous_index] = self.effective_requirement_check(check) + seen_identities[identity] = (check, previous_index) + else: + profiles = {previous_profile.identifier, source_profile.identifier} + raise DuplicateRequirementCheck( + f"{check.name} [{check.severity.name}] from {', '.join(sorted(profiles))}", + self.identifier, + ) + else: + seen_identities[identity] = (check, len(effective_checks)) + effective_checks.append(self.effective_requirement_check(check)) + return tuple(effective_checks) + + def validate_checks(self) -> tuple[ProfileCheckResult, ...]: + """Run and cache the registered consistency checks for this profile.""" + cache_key = (self.path.resolve(), self.severity) + if cache_key not in self.__profile_check_results: + self.__profile_check_results[cache_key] = ProfileCheckSuite().run(self) + return self.__profile_check_results[cache_key] + + def validate_profile_checks(self) -> None: + """Run all profile checks and raise when one of them fails.""" + for result in self.validate_checks(): + if result.passed: + continue + if result.check_id == "unique-requirement-check-identity": + name = result.details["name"] + severity = result.details["severity"] + raise DuplicateRequirementCheck(f"{name} [{severity}]", self.identifier) + raise ProfileCheckFailure(result) + + def validate_requirement_check_identities(self) -> None: + """Backward-compatible alias for the profile check suite.""" + self.validate_profile_checks() @classmethod def __get_nested_profiles__(cls, source: str) -> list[str]: @@ -432,16 +600,25 @@ def __get_nested_profiles__(cls, source: str) -> list[str]: @property def inherited_profiles(self) -> list[Profile]: inherited_profiles = self.is_transitive_profile_of - if not inherited_profiles or len(inherited_profiles) == 0: + if not inherited_profiles: inherited_profiles = Profile.__get_nested_profiles__(self.uri) profile_keys = self.__profiles_map.keys - return [ + profiles = [ profile for key in inherited_profiles if key in profile_keys for profile in [self.__profiles_map.get_by_key(key)] if isinstance(profile, Profile) ] + # Neither RDF values nor the ancestry walk guarantee dependency order. + # Reuse the stable topological sorter used for requirements and checks. + positions = {profile.uri: index for index, profile in enumerate(profiles)} + edges: dict[int, set[int]] = {} + for child_index, profile in enumerate(profiles): + for parent_uri in profile.is_profile_of: + if parent_uri in positions: + edges.setdefault(positions[parent_uri], set()).add(child_index) + return RequirementLoader._topological_order(profiles, edges, self.identifier, "profiles") def add_requirement(self, requirement: Requirement): self._requirements.append(requirement) @@ -595,13 +772,11 @@ def __load_profiles_paths__( if not profile_root_directory.is_dir(): raise InvalidProfilePath(str(profile_root_directory)) # if the path is a directory, get the profile directories - result.extend( - [ - (profile_root_directory, p.parent) - for p in profile_root_directory.rglob("*.*") - if p.name == PROFILE_SPECIFICATION_FILE - ] - ) + result.extend([ + (profile_root_directory, p.parent) + for p in profile_root_directory.rglob("*.*") + if p.name == PROFILE_SPECIFICATION_FILE + ]) # return the list of profile directories return result @@ -654,19 +829,12 @@ def load_profiles( # Check for overridden checks if not allow_requirement_check_override: - # Navigate the profiles to check for overridden checks. - # If the override is not enabled in the settings raise an error. - profiles_checks = set() - # Search for duplicated checks in the profiles + override_check_suite = ProfileCheckSuite(checks=(NoRequirementCheckOverrides,)) for profile in profiles: - profile_checks = [_ for r in profile.get_requirements() for _ in r.get_checks()] - for check in profile_checks: - # If the check is already present in the list of checks, - # raise an error if the override is not enabled. - if check in profiles_checks: - raise DuplicateRequirementCheck(check.name, profile.identifier) - # Add the check to the list of checks - profiles_checks.add(check) + result = override_check_suite.run(profile)[0] + if not result.passed: + identity = f"{result.details['name']} [{result.details['severity']}]" + raise DuplicateRequirementCheck(identity, profile.identifier) # order profiles according to the number of profiles they depend on: # i.e, first the profiles that do not depend on any other profile @@ -768,10 +936,62 @@ def all(cls) -> list[Profile]: """ return list(cls.__profiles_map.values()) + @staticmethod + def version_sort_key(version: str | None) -> tuple[int, ...]: + """ + Sort key for a profile version. + + The version is compared component by component as a tuple of integers, so that + e.g. `1.10` sorts after `1.9` (a plain string comparison would get this wrong). + Non-numeric components and missing versions sort lowest. + + :param version: the version string + :type version: str | None + + :return: the sort key + :rtype: tuple[int, ...] + """ + if not version: + return () + return tuple(int(part) if part.isdigit() else 0 for part in str(version).split(".")) + + @classmethod + def resolve_in_list(cls, profiles: Collection[Profile], profile_identifier: str) -> Profile | None: + """ + Resolve the given identifier against the given list of profiles. + + An exact match on the profile identifier (e.g. `ro-crate-1.3`) always wins. + Failing that, the identifier is treated as a bare token (e.g. `ro-crate`) and + resolves to the **highest available version** of that token. + + :param profiles: the list of profiles + :type profiles: Collection[Profile] + + :param profile_identifier: the identifier or token + :type profile_identifier: str + + :return: the profile if found, None otherwise + :rtype: Optional[Profile] + """ + exact_match = next((p for p in profiles if p.identifier == profile_identifier), None) + if exact_match is not None: + return exact_match + candidates = [ + p + for p in profiles + if p.token == profile_identifier or str(p.identifier).replace(f"-{p.version}", "") == profile_identifier + ] + if not candidates: + return None + return max(candidates, key=lambda p: cls.version_sort_key(p.version)) + @classmethod def find_in_list(cls, profiles: Collection[Profile], profile_identifier: str) -> Profile | None: """ - Find a profile with the given identifier in the given list of profiles + Find a profile with the given identifier in the given list of profiles. + + Resolution follows :meth:`resolve_in_list`: an exact identifier match wins, + otherwise a bare token resolves to the highest available version. :param profiles: the list of profiles :type profiles: Collection[Profile] @@ -782,10 +1002,7 @@ def find_in_list(cls, profiles: Collection[Profile], profile_identifier: str) -> :return: the profile if found, None otherwise :rtype: Optional[Profile] """ - profile = next((p for p in profiles if p.identifier == profile_identifier), None) or next( - (p for p in profiles if str(p.identifier).replace(f"-{p.version}", "") == profile_identifier), - None, - ) + profile = cls.resolve_in_list(profiles, profile_identifier) if not profile: raise ProfileNotFound(profile_identifier) return profile diff --git a/rocrate_validator/models/profile_check.py b/rocrate_validator/models/profile_check.py new file mode 100644 index 000000000..43f034e23 --- /dev/null +++ b/rocrate_validator/models/profile_check.py @@ -0,0 +1,200 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, ClassVar + +if TYPE_CHECKING: + from collections.abc import Mapping + + from rocrate_validator.models.profile import Profile + from rocrate_validator.models.severity import Severity + + +@dataclass(frozen=True) +class ProfileCheckResult: + """Structured result produced by a profile consistency check.""" + + check_id: str + profile_identifier: str + passed: bool + message: str + details: Mapping[str, str] = field(default_factory=dict) + + +class ProfileCheckFailure(Exception): + """Raised when a profile consistency check does not pass.""" + + def __init__(self, result: ProfileCheckResult): + self.result = result + super().__init__(result.message) + + +class ProfileCheck(ABC): + """Base class for checks validating a profile definition.""" + + identifier: ClassVar[str] + description: ClassVar[str] + + @abstractmethod + def run(self, profile: Profile) -> ProfileCheckResult: + """Run this check against ``profile``.""" + + +class UniqueRequirementCheckIdentity(ProfileCheck): + """Ensure ``(name, severity)`` identifies at most one check per profile.""" + + identifier = "unique-requirement-check-identity" + description = "Requirement check names and severities must be unique within a profile" + + def run(self, profile: Profile) -> ProfileCheckResult: + seen: set[tuple[str, Severity]] = set() + for requirement in profile.requirements: + for check in requirement.get_checks(): + identity = (check.name, check.severity) + if identity in seen: + return ProfileCheckResult( + check_id=self.identifier, + profile_identifier=profile.identifier, + passed=False, + message="Duplicate requirement check identity", + details={ + "name": check.name, + "severity": check.severity.name, + }, + ) + seen.add(identity) + return ProfileCheckResult( + check_id=self.identifier, + profile_identifier=profile.identifier, + passed=True, + message="All requirement check identities are unique", + ) + + +class RuleOverlayConsistency(ProfileCheck): + """Ensure rule overlay sources can be composed without ambiguity.""" + + identifier = "rule-overlay-consistency" + description = "Rule overlay sources must be loaded direct parents with distinct check identities" + + def run(self, profile: Profile) -> ProfileCheckResult: + sources = profile.is_rule_overlay_of + if not sources: + return ProfileCheckResult( + check_id=self.identifier, + profile_identifier=profile.identifier, + passed=True, + message="Profile does not declare rule overlay sources", + ) + + declared_parent_uris = set(profile.is_profile_of) + loaded_parents = {parent.uri: parent for parent in profile.parents} + for source_uri in sources: + if source_uri not in declared_parent_uris: + return ProfileCheckResult( + check_id=self.identifier, + profile_identifier=profile.identifier, + passed=False, + message="Rule overlay source is not a direct parent", + details={"source": source_uri}, + ) + if source_uri not in loaded_parents: + return ProfileCheckResult( + check_id=self.identifier, + profile_identifier=profile.identifier, + passed=False, + message="Rule overlay source profile is not loaded", + details={"source": source_uri}, + ) + + identities: dict[tuple[str, Severity], str] = {} + for source_uri in sources: + source_profile = loaded_parents[source_uri] + for requirement in source_profile.requirements: + for check in requirement.get_checks(): + identity = (check.name, check.severity) + previous_source = identities.get(identity) + if previous_source is not None and previous_source != source_uri: + return ProfileCheckResult( + check_id=self.identifier, + profile_identifier=profile.identifier, + passed=False, + message="Rule overlay sources contain an ambiguous check identity", + details={ + "name": check.name, + "severity": check.severity.name, + "sources": f"{previous_source}, {source_uri}", + }, + ) + identities[identity] = source_uri + + return ProfileCheckResult( + check_id=self.identifier, + profile_identifier=profile.identifier, + passed=True, + message="Rule overlay sources are consistent", + ) + + +class NoRequirementCheckOverrides(ProfileCheck): + """Ensure a profile does not replace checks defined by direct parents.""" + + identifier = "no-requirement-check-overrides" + description = "Requirement check overrides must not be present" + + def run(self, profile: Profile) -> ProfileCheckResult: + for requirement in profile.requirements: + for check in requirement.get_checks(): + overridden_checks = check.overrides + if overridden_checks: + return ProfileCheckResult( + check_id=self.identifier, + profile_identifier=profile.identifier, + passed=False, + message="Requirement check override is disabled", + details={ + "name": check.name, + "severity": check.severity.name, + "sources": ", ".join( + overridden.requirement.profile.identifier for overridden in overridden_checks + ), + }, + ) + return ProfileCheckResult( + check_id=self.identifier, + profile_identifier=profile.identifier, + passed=True, + message="No requirement check overrides found", + ) + + +class ProfileCheckSuite: + """Run the registered consistency checks for a profile.""" + + DEFAULT_CHECKS: ClassVar[tuple[type[ProfileCheck], ...]] = ( + UniqueRequirementCheckIdentity, + RuleOverlayConsistency, + ) + + def __init__(self, checks: tuple[type[ProfileCheck], ...] | None = None): + selected_checks = checks if checks is not None else self.DEFAULT_CHECKS + self._checks = tuple(check() for check in selected_checks) + + def run(self, profile: Profile) -> tuple[ProfileCheckResult, ...]: + """Return one result for every registered profile check.""" + return tuple(check.run(profile) for check in self._checks) diff --git a/rocrate_validator/models/profile_detection.py b/rocrate_validator/models/profile_detection.py new file mode 100644 index 000000000..cffc0a9a3 --- /dev/null +++ b/rocrate_validator/models/profile_detection.py @@ -0,0 +1,213 @@ +"""Structured results returned by automatic profile detection.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +from rocrate_validator.models._logging import logger +from rocrate_validator.models.profile import Profile +from rocrate_validator.rocrate.entity import ROCrateEntity + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + + from rocrate_validator.models.validation import ValidationContext + + +@dataclass(frozen=True) +class ProfileDetectionDiagnostic: + """Explain a malformed, missing, or unrecognized profile-detection value.""" + + code: str + property: str | None + location: str | None + value: Any + message: str + + +@dataclass +class ProfileDetectionResult: + """Detected profiles, diagnostics, and an optional user-confirmable suggestion.""" + + profiles: list[Profile] + diagnostics: list[ProfileDetectionDiagnostic] + suggested_profile: Profile | None = None + + +def detect_rocrate_profiles( + context: ValidationContext, + profile_loader: Callable[[], Sequence[Profile]] | None = None, +) -> ProfileDetectionResult: + """Detect profiles from an existing context and describe unresolved metadata hints.""" + + # Keep string URIs as suggestion hints, not valid profile references. + hints, malformed_values = _get_profile_hints(context.ro_crate.metadata) + candidate_profile_uris = {value.id for _, value in hints if isinstance(value.id, str) and value.id} + diagnostics = [ + ProfileDetectionDiagnostic( + code="INVALID_REFERENCE" if prop == "about" else "INVALID_CONFORMS_TO", + property=prop, + location=location, + value=value, + message=( + "Expected a JSON-LD entity reference containing `@id`, not a string. " + f'Use {{"@id": {json.dumps(value)}}} instead. ' + "The metadata has not been changed." + if prop == "conformsTo" and isinstance(value, str) + else "Expected a JSON-LD reference object containing `@id`; " + 'for example, {"@id": "https://example.org/profile"}.' + ), + ) + for prop, location, value in malformed_values + ] + for diagnostic in diagnostics: + logger.warning( + "Non-conforming %s `%s` value %r: %s", + diagnostic.location, + diagnostic.property, + diagnostic.value, + diagnostic.message, + ) + + logger.debug("Candidate profiles: %s", candidate_profile_uris) + has_malformed_profile_hint = any(prop == "conformsTo" for prop, _, _ in malformed_values) + has_string_profile_hint = any( + prop == "conformsTo" and isinstance(value, str) for prop, _, value in malformed_values + ) + + # No hint at all is different from a malformed hint: report absence only + # when the crate supplied no conformsTo value that we could inspect. + if not candidate_profile_uris and not has_malformed_profile_hint: + diagnostics.append( + ProfileDetectionDiagnostic( + code="NO_PROFILE_HINT", + property="conformsTo", + location=None, + value=None, + message="No `conformsTo` profile reference was found in the metadata descriptor or Root Data Entity.", + ) + ) + logger.debug("Unable to determine the profile to validate against") + + # Load the catalog only for valid references or potential string suggestions. + if not candidate_profile_uris and not has_string_profile_hint: + logger.debug("No usable profile URI found") + return ProfileDetectionResult([], diagnostics) + + # Only valid entity references participate in automatic profile selection. + if profile_loader is None: + available_profiles = Profile.load_profiles( + context.profiles_path, + extra_profiles_path=context.extra_profiles_path, + publicID=context.publicID, + severity=context.requirement_severity, + ) + else: + available_profiles = profile_loader() + matched_profiles = [profile for profile in available_profiles if profile.uri in candidate_profile_uris] + candidate_profiles = [] + for profile in matched_profiles: + candidate_profiles.append(profile) + for inherited_profile in profile.inherited_profiles: + # Keep the most specific matched profiles; their inherited + # profiles are applied through the inheritance relationship. + if inherited_profile in candidate_profiles: + candidate_profiles.remove(inherited_profile) + logger.debug("%d Candidate Profiles found: %s", len(candidate_profiles), candidate_profiles) + + unmatched_profile_uris = candidate_profile_uris.difference({profile.uri for profile in matched_profiles}) + logger.debug("Unmatched Candidate Profiles URIs: %s", unmatched_profile_uris) + if unmatched_profile_uris: + logger.warning( + "The conformance to the following profiles could not be verified: %s", + ", ".join(unmatched_profile_uris), + ) + for profile_uri in unmatched_profile_uris: + locations = [location for location, value in hints if value.id == profile_uri] + locations.extend( + location for prop, location, value in malformed_values if prop == "conformsTo" and value == profile_uri + ) + diagnostics.append( + ProfileDetectionDiagnostic( + code="PROFILE_NOT_FOUND", + property="conformsTo", + location=", ".join(locations), + value=profile_uri, + message="No available validation profile has this URI.", + ) + ) + + suggested_profile = _suggest_profile(candidate_profiles, candidate_profile_uris, diagnostics, available_profiles) + result = ProfileDetectionResult(candidate_profiles, diagnostics, suggested_profile) + logger.debug("Detailed profile detection result: %s", result) + return result + + +def _get_profile_hints(metadata) -> tuple[list[tuple[str, ROCrateEntity]], list[tuple[str, str, Any]]]: + """Read descriptor and Root Data Entity profile hints and retain their locations.""" + hints: list[tuple[str, ROCrateEntity]] = [] + diagnostics: list[tuple[str, str, Any]] = [] + try: + descriptor = metadata.get_file_descriptor_entity() + except json.JSONDecodeError: + # JSONDecodeError is also a ValueError, but invalid JSON must not be + # misreported as a missing descriptor or missing conformsTo hint. + raise + except ValueError: + # Without a descriptor there is no reliable source for either the + # descriptor's conformsTo or the Root Data Entity's hints. Other + # exceptions indicate unexpected failures and must reach the caller. + return hints, diagnostics + + root_data_entity = descriptor.get_property("about") + entities: list[tuple[str, ROCrateEntity]] = [("RO-Crate Metadata Descriptor", descriptor)] + if isinstance(root_data_entity, ROCrateEntity): + entities.append(("Root Data Entity", root_data_entity)) + elif root_data_entity is not None: + # `about` should point to an entity. Preserve a wrong-shaped value as a + # diagnostic rather than attempting to read properties from it. + diagnostics.append(("about", "RO-Crate Metadata Descriptor", root_data_entity)) + + for location, entity in entities: + raw_values = entity.get_property("conformsTo", []) + # JSON-LD permits one value or an array of values for a property. + values = raw_values if isinstance(raw_values, list) else [raw_values] + for value in values: + if value is None: + continue + if isinstance(value, ROCrateEntity) and isinstance(value.id, str) and value.id: + hints.append((location, value)) + else: + diagnostics.append(("conformsTo", location, value)) + return hints, diagnostics + + +def _suggest_profile( + candidates: list[Profile], + candidate_profile_uris: set[str], + diagnostics: list[ProfileDetectionDiagnostic], + available_profiles: Sequence[Profile], +) -> Profile | None: + """Return one profile only when malformed string form is the sole detection issue.""" + string_diagnostics = [diagnostic for diagnostic in diagnostics if diagnostic.code == "INVALID_CONFORMS_TO"] + + # Do not guess if a valid reference, another kind of metadata problem, or + # conflicting string values could change which profile the crate intends. + can_suggest = ( + not candidates + and not candidate_profile_uris + and len(string_diagnostics) == len(diagnostics) + and bool(string_diagnostics) + and all(isinstance(diagnostic.value, str) for diagnostic in string_diagnostics) + and len({diagnostic.value for diagnostic in string_diagnostics}) == 1 + ) + if not can_suggest: + return None + + # Matching must be unique across available profiles; zero matches means + # unknown URI, while multiple matches would make the suggestion ambiguous. + suggestion_uri = string_diagnostics[0].value + matches = [profile for profile in available_profiles if profile.uri == suggestion_uri] + return matches[0] if len(matches) == 1 else None diff --git a/rocrate_validator/models/profile_provenance.py b/rocrate_validator/models/profile_provenance.py new file mode 100644 index 000000000..fca71d436 --- /dev/null +++ b/rocrate_validator/models/profile_provenance.py @@ -0,0 +1,67 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from rocrate_validator.models.profile import Profile + from rocrate_validator.models.requirement import RequirementCheck + + +class RequirementCheckRelation(Enum): + """How a requirement check became part of an effective profile.""" + + DEFINED_LOCALLY = "defined_locally" + INHERITED = "inherited" + REPLACES = "replaces" + + +@dataclass(frozen=True) +class EffectiveRequirementCheck: + """ + A requirement check as exposed by a specific validation profile. + + The source check is never mutated. ``identifier`` and ``profile`` are the + identity under which the check is reported by the effective profile, while + the ``source_*`` properties retain the implementation provenance. + """ + + check: RequirementCheck + identifier: str + profile: Profile + relation: RequirementCheckRelation + replaces: tuple[RequirementCheck, ...] = () + + @property + def source_identifier(self) -> str: + return self.check.identifier + + @property + def source_profile(self) -> Profile: + return self.check.requirement.profile + + def to_dict(self) -> dict[str, Any]: + """Return a serializable representation of the effective check.""" + return { + "identifier": self.identifier, + "profile": self.profile.identifier, + "source_identifier": self.source_identifier, + "source_profile": self.source_profile.identifier, + "relation": self.relation.value, + "replaces": [check.identifier for check in self.replaces], + } diff --git a/rocrate_validator/models/requirement.py b/rocrate_validator/models/requirement.py index 7ef85296b..568b9176c 100644 --- a/rocrate_validator/models/requirement.py +++ b/rocrate_validator/models/requirement.py @@ -15,7 +15,6 @@ from __future__ import annotations import importlib -import json from abc import ABC, abstractmethod from dataclasses import dataclass from functools import total_ordering @@ -26,14 +25,22 @@ PROFILE_FILE_EXTENSIONS, PROFILE_SPECIFICATION_FILE, ) +from rocrate_validator.errors import CheckDependencyError, ROCrateMetadataNotFoundError, ValidationExecutionError from rocrate_validator.events import EventType from rocrate_validator.models._logging import logger +from rocrate_validator.models.check_result import ( + CheckResult, + CheckResultValue, + DeferredRequirementCheck, + normalize_check_result, +) +from rocrate_validator.models.events import RequirementCheckValidationEvent from rocrate_validator.models.severity import ( LevelCollection, RequirementLevel, Severity, ) -from rocrate_validator.utils import log as logging +from rocrate_validator.models.skipped_check import SkipCategory, SkipRequirementCheck from rocrate_validator.utils.python_helpers import ( get_requirement_name_from_file, ) @@ -44,14 +51,9 @@ from rocrate_validator.models.profile import Profile from rocrate_validator.models.validation import ValidationContext - -class SkipRequirementCheck(Exception): - def __init__(self, check: RequirementCheck, message: str = ""): - self.check = check - self.message = message - - def __str__(self): - return f"SkipRequirementCheck(check={self.check})" +# This module contains the requirement/check model and its loader algorithms. +# Keep the related model code together; splitting it would create circular imports. +# pylint: disable=too-many-lines @total_ordering @@ -186,6 +188,12 @@ def __init_checks__(self) -> list[RequirementCheck]: def get_checks(self) -> list[RequirementCheck]: return self._checks.copy() + def set_checks_order(self, checks: list[RequirementCheck]) -> None: + """Replace the check order and refresh the public check numbers.""" + self._checks = checks.copy() + for i, check in enumerate(self._checks, start=1): + check.order_number = i + def get_check(self, name: str) -> RequirementCheck | None: for check in self._checks: if check.name == name: @@ -212,38 +220,52 @@ def _do_validate_(self, context: ValidationContext) -> bool: len(self._checks), ) all_passed = True - checks_to_perform = [ - _ - for _ in self._checks - if not context.settings.skip_checks or _.identifier not in context.settings.skip_checks - ] + checks_to_perform = [check for check in self._checks if not context.is_check_skipped(check)] + configured_skips = [check for check in self._checks if check not in checks_to_perform] + for check in configured_skips: + message = context.check_skip_reason(check) + context.result._record_check_result( + check, + CheckResult.SKIPPED, + message, + SkipCategory.CONFIGURED, + ) + processed_checks: set[RequirementCheck] = set() for check in checks_to_perform: + processed_checks.add(check) + dependency_skip_reason = self.__dependency_skip_reason__(check, context) + if dependency_skip_reason: + logger.debug("Skipping check '%s' because: %s", check.name, dependency_skip_reason) + self.__record_dependency_skip__(check, context, dependency_skip_reason) + continue try: all_passed, should_break = self.__execute_check__(check, context, all_passed) if should_break: break + except DeferredRequirementCheck: + logger.debug("Check '%s' is pending batched validation", check.name) + continue except SkipRequirementCheck as e: logger.debug("Skipping check '%s' because: %s", check.name, e) - context.result._add_skipped_check(check) + self.__record_skipped_check__(check, context, e.message or "Check requested a skip", e.category) continue - except json.JSONDecodeError as e: - # The file descriptor is not valid JSON, so this check could not run. - # This is a malformed-input problem (reported as an ad-hoc issue by the - # dedicated "File Descriptor JSON format" check), not a validator bug. - logger.debug("Skipping check %s: file descriptor is not valid JSON: %s", check, e) except Exception as e: - if context.maybe_warn_offline_cache_miss(e): - logger.debug("Offline cache miss during check %s: %s", check, e) - else: - logger.warning("Unexpected error during check %s. Exception: %s", check, e) - logger.warning("Consider reporting this as a bug.") - if logger.isEnabledFor(logging.DEBUG): - logger.exception("Unhandled exception during check execution", exc_info=e) + self.__handle_check_error__(check, context, e) # Stop running further checks once the metadata is known to be unusable. if context.aborted: break - skipped_checks = set(self._checks) - set(checks_to_perform) - context.result.skipped_checks.update(skipped_checks) + self.__record_skipped_checks__( + configured_skips, + context, + "Check was skipped by validation settings", + SkipCategory.CONFIGURED, + ) + self.__record_skipped_checks__( + set(checks_to_perform) - processed_checks, + context, + "Validation stopped before this check was executed", + SkipCategory.NOT_REACHED, + ) logger.debug( "Checks for Requirement '%s' completed. Checks passed? %s", self.name, @@ -251,26 +273,188 @@ def _do_validate_(self, context: ValidationContext) -> bool: ) return all_passed - def __execute_check__(self, check, context, all_passed): - from rocrate_validator.models.events import ( # noqa: PLC0415 - RequirementCheckValidationEvent, + @staticmethod + def __handle_check_error__(check, context, error: Exception) -> None: + if isinstance(error, (FileNotFoundError, ROCrateMetadataNotFoundError)): + # A missing descriptor/metadata graph is an input problem reported by + # the dedicated descriptor checks, not an implementation failure. + logger.debug("Skipping check %s: validation input is unavailable: %s", check, error) + return + + if isinstance(error, ValidationExecutionError): + # An engine-level failure means validation did not complete. Do not + # turn it into a warning and accidentally return a clean result. + raise error.with_traceback(error.__traceback__) + + if context.maybe_warn_offline_cache_miss(error): + logger.debug("Offline cache miss during check %s: %s", check, error) + return + + check_path = str(check.requirement.path) if check.requirement.path else check.identifier + raise ValidationExecutionError( + message=f"Unexpected error while executing check '{check.identifier}': {type(error).__name__}: {error}", + path=check_path, + ) from error + + @staticmethod + def __record_skipped_check__( + check, + context, + message: str = "Check was skipped", + category: SkipCategory = SkipCategory.RETURNED, + ) -> None: + # Overlay replacements and batched SHACL checks may already have an + # outcome before traversal reaches their own requirement. Aborting + # must not overwrite that outcome or emit a misleading skipped event. + if category is SkipCategory.NOT_REACHED and context.result.get_check_result(check) is not None: + return + + context.result._record_check_result(check, CheckResult.SKIPPED, message, category) + inherited_reporting_disabled = ( + check.requirement.profile.identifier != context.profile_identifier + and not context.is_rule_overlay_source(check.requirement.profile) + and context.settings.disable_inherited_profiles_issue_reporting ) + if not inherited_reporting_disabled: + context.validator.notify( + RequirementCheckValidationEvent( + EventType.REQUIREMENT_CHECK_VALIDATION_END, + check, + validation_result=CheckResult.SKIPPED, + message=message, + ) + ) - if check.overridden and check.requirement.profile.identifier != context.profile_identifier: + @classmethod + def __record_skipped_checks__( + cls, + checks, + context, + message: str = "Check was skipped", + category: SkipCategory = SkipCategory.RETURNED, + ) -> None: + for check in sorted(checks): + cls.__record_skipped_check__(check, context, message, category) + + @classmethod + def record_skipped_checks(cls, requirements: list[Requirement], context: ValidationContext) -> None: + """Record checks belonging to requirements that validation will not reach.""" + message = "Validation stopped before this check was executed" + if context.abort_reason: + message = f"Validation stopped before this check was executed: {context.abort_reason}" + for requirement in requirements: + cls.__record_skipped_checks__(requirement.get_checks(), context, message, SkipCategory.NOT_REACHED) + + @staticmethod + def __dependency_skip_reason__(check: RequirementCheck, context: ValidationContext) -> str | None: + """ + Explain why ``check`` cannot run because a dependency did not pass. + + Dependency lookup is delegated to the validation context so an overlay + can resolve inherited checks and target-local replacements. ``None`` + means that every declared dependency has already passed. + """ + if not check.depends_on: + return None + + blocked_dependencies = [] + for dependency_name in check.depends_on: + dependency = context.resolve_dependency_check(check, dependency_name) + dependency_result = context.result.get_check_result(dependency) + if dependency_result is not CheckResult.PASSED: + result_name = dependency_result.value if dependency_result else "not processed" + blocked_dependencies.append(f"{dependency.name} ({result_name})") + + if not blocked_dependencies: + return None + return "Check dependency did not pass: " + ", ".join(blocked_dependencies) + + @staticmethod + def __record_dependency_skip__(check, context: ValidationContext, message: str) -> None: + Requirement.__record_skipped_check__(check, context, message, SkipCategory.DEPENDENCY) + + @staticmethod + def __resolve_execution_check__( + check: RequirementCheck, + context: ValidationContext, + ) -> RequirementCheck | None: + """ + Resolve the check that must execute in the current source-profile slot. + + Ordinary checks are returned unchanged. When a source Python check is + replaced by the active overlay, its target-local replacement is returned + so it runs at the same point in the general-to-specific traversal. SHACL + replacements return ``None`` because their shapes are collected across + profiles and evaluated together when validation reaches the target. + + ``None`` also indicates that the replacement has already run or that no + replacement belonging to the active target can be resolved. + """ + if not check.overridden or check.requirement.profile.identifier == context.profile_identifier: + return check + + from rocrate_validator.requirements.python import PyFunctionCheck # noqa: PLC0415 + + # Python checks execute independently, so run the target replacement + # in the source check's slot to preserve general-to-specific ordering. + # SHACL checks are different: their shapes are collected across all + # profiles and evaluated once at the target profile. Executing a SHACL + # replacement here would start that validation too early. + if not isinstance(check, PyFunctionCheck): + return None + + replacement = context.effective_check_replacement(check) + if replacement is None: logger.debug( "Skipping check '%s' because overridden by '%r'", check.identifier, [_.identifier for _ in check.overridden_by], ) + return None + if context.result.get_check_result(replacement) is not None: + return None + logger.debug( + "Executing target replacement '%s' at source check '%s'", + replacement.identifier, + check.identifier, + ) + return replacement + + def __execute_check__( + self, + check: RequirementCheck, + context: ValidationContext, + all_passed: bool, + ) -> tuple[bool, bool]: + """ + Execute one effective check and update the enclosing requirement state. + + Overlay replacements are resolved before execution, and a replacement + already executed in its source slot is not run again when the target + profile is visited. The returned tuple contains the accumulated pass + state and whether fail-fast processing must stop after this check. + """ + # An overlay replacement may already have run in the source profile's + # slot. Keep the target pass a no-op instead of executing the same + # Python check twice. + recorded_result = context.result.get_check_result(check) + if recorded_result is not None: + failed = recorded_result is CheckResult.FAILED and not check.extra_check + return all_passed and not failed, failed and context.fail_fast + + execution_check = self.__resolve_execution_check__(check, context) + if execution_check is None: return all_passed, False + check = execution_check if check.deactivated: logger.debug("Skipping check '%s' because deactivated", check.identifier) - context.result._add_skipped_check(check) + self.__record_skipped_check__(check, context, "Check is deactivated", SkipCategory.DEACTIVATED) return all_passed, False # Determine whether to skip event notification for inherited profiles skip_event_notify = False if ( check.requirement.profile.identifier != context.profile_identifier + and not context.is_rule_overlay_source(check.requirement.profile) and context.settings.disable_inherited_profiles_issue_reporting ): logger.debug( @@ -287,33 +471,38 @@ def __execute_check__(self, check, context, all_passed): ) # Execute the check and get the result check_result = check.execute_check(context) - logger.debug("Result of check %s: %s", check.identifier, check_result) - context.result._add_executed_check(check, check_result) + normalized_result = normalize_check_result(check_result) + logger.debug("Result of check %s: %s", check.identifier, normalized_result.value) + skip_message = None + skip_category = SkipCategory.RETURNED + if skip_event_notify and normalized_result is CheckResult.SKIPPED: + skip_message = "Inherited profile issue reporting is disabled" + skip_category = SkipCategory.INHERITED + context.result._record_check_result(check, normalized_result, skip_message, skip_category) # Notify the end of the check execution if not skip_event_notify is set to True if not skip_event_notify: context.validator.notify( RequirementCheckValidationEvent( EventType.REQUIREMENT_CHECK_VALIDATION_END, check, - validation_result=check_result, + validation_result=normalized_result, + message=skip_message, ) ) logger.debug( "Ran check '%s'. Got result %s", check.identifier, - check_result, + normalized_result, ) - # Ensure the check result is a boolean value, otherwise log a warning and ignore the check result - if not isinstance(check_result, bool): - logger.warning( - "Ignoring the check %s as it returned the value %r instead of a boolean", - check.name, - check_result, - ) - raise TypeError(f"Ignoring invalid result from check {check.name}") - new_all_passed = all_passed and check_result - should_break = not new_all_passed and context.fail_fast - return new_all_passed, should_break + new_all_passed = all_passed and (check.extra_check or normalized_result is not CheckResult.FAILED) + # A batched backend can also report failures for checks other than the + # one driving this execution. + should_break = ( + not check.extra_check + and context.fail_fast + and (normalized_result is CheckResult.FAILED or context.result.has_issues()) + ) + return new_all_passed and not should_break, should_break def __eq__(self, other: object) -> bool: if not isinstance(other, Requirement): @@ -427,14 +616,7 @@ def __get_requirement_classes__() -> list[type[Requirement]]: # Ensure known requirement modules are imported so subclasses are registered. for requirement_type in ("python", "shacl"): module_name = f"rocrate_validator.requirements.{requirement_type}" - try: - importlib.import_module(module_name) - except Exception: - logger.debug( - "Unable to import requirement module: %s", - module_name, - exc_info=True, - ) + importlib.import_module(module_name) def all_subclasses( base_class: type[Requirement], @@ -500,6 +682,7 @@ def ok_file(p: Path) -> bool: ), reverse=False, ) + requirements = RequirementLoader.order_by_dependencies(requirements, profile=profile) # assign order numbers to requirements for i, requirement in enumerate(requirements): requirement._order_number = i + 1 @@ -507,6 +690,193 @@ def ok_file(p: Path) -> bool: logger.debug("Profile %s loaded %s requirements: %s", profile.identifier, len(requirements), requirements) return requirements + @staticmethod + def _check_index(requirements: list[Requirement]) -> dict[str, list[RequirementCheck]]: + checks_by_name: dict[str, list[RequirementCheck]] = {} + for requirement in requirements: + for check in requirement.get_checks(): + checks_by_name.setdefault(check.name, []).append(check) + return checks_by_name + + @classmethod + def effective_check_index(cls, profile: Profile) -> dict[str, list[RequirementCheck]]: + """ + Index the checks active in ``profile`` after inherited replacements. + + Checks are collected from the target and all inherited profiles. A + source check is excluded only when another check in this composition + explicitly overrides it; unrelated matches remain in the index so a + name-only dependency is correctly rejected as ambiguous. + """ + profiles = (profile, *profile.inherited_profiles) + requirements = [requirement for source_profile in profiles for requirement in source_profile.requirements] + checks = [check for requirement in requirements for check in requirement.get_checks()] + check_ids = {id(check) for check in checks} + shadowed_ids = { + id(overridden) for check in checks for overridden in check.overrides if id(overridden) in check_ids + } + return { + name: [check for check in matches if id(check) not in shadowed_ids] + for name, matches in cls._check_index(requirements).items() + } + + @staticmethod + def _resolve_dependency( + check: RequirementCheck, + dependency_name: str, + checks_by_name: dict[str, list[RequirementCheck]], + ) -> RequirementCheck: + candidates = checks_by_name.get(dependency_name, []) + profile_name = check.requirement.profile.identifier + if not candidates: + raise CheckDependencyError( + f"check {check.name!r} depends on unknown check {dependency_name!r}", + profile_name, + ) + if len(candidates) > 1: + raise CheckDependencyError( + f"check {check.name!r} depends on ambiguous check {dependency_name!r}", + profile_name, + ) + return candidates[0] + + @staticmethod + def _topological_order( + nodes: list[Any], edges: dict[int, set[int]], profile_name: str, node_type: str + ) -> list[Any]: + indegree = [0] * len(nodes) + for targets in edges.values(): + for target in targets: + indegree[target] += 1 + + ready = [index for index, degree in enumerate(indegree) if degree == 0] + ordered_indices: list[int] = [] + while ready: + ready.sort() + index = ready.pop(0) + ordered_indices.append(index) + for target in sorted(edges.get(index, ())): + indegree[target] -= 1 + if indegree[target] == 0: + ready.append(target) + + if len(ordered_indices) != len(nodes): + cycle_nodes = [nodes[index] for index, degree in enumerate(indegree) if degree > 0] + cycle_names = ", ".join(getattr(node, "name", str(node)) for node in cycle_nodes) + raise CheckDependencyError(f"dependency cycle detected among {node_type}: {cycle_names}", profile_name) + + return [nodes[index] for index in ordered_indices] + + # The topological ordering algorithm keeps its graph bookkeeping local. + # pylint: disable=too-many-locals + @classmethod + def order_by_dependencies( + cls, + requirements: list[Requirement], + *, + profile: Profile | None = None, + ) -> list[Requirement]: + """ + Order local checks and requirements according to their dependencies. + + Dependencies within ``requirements`` produce topological-order edges. + A dependency supplied by an inherited profile is already guaranteed to + execute earlier in the profile traversal, so it is accepted without + adding a local edge. Unknown local dependencies and cycles still fail + profile loading. + """ + if not requirements: + return [] + + checks_by_name = cls._check_index(requirements) + requirement_indices = {id(requirement): index for index, requirement in enumerate(requirements)} + requirement_edges: dict[int, set[int]] = {} + + for requirement in requirements: + checks = requirement.get_checks() + check_indices = {id(check): index for index, check in enumerate(checks)} + check_edges: dict[int, set[int]] = {} + for check in checks: + for dependency_name in check.depends_on: + inherited_dependency = ( + dependency_name not in checks_by_name + and profile is not None + and any( + inherited.get_requirement_check(dependency_name) is not None + for inherited in profile.inherited_profiles + ) + ) + if inherited_dependency: + continue + dependency = cls._resolve_dependency(check, dependency_name, checks_by_name) + dependency_requirement_index = requirement_indices.get(id(dependency.requirement)) + if dependency_requirement_index is None: + continue + source_requirement_index = requirement_indices[id(requirement)] + if dependency_requirement_index == source_requirement_index: + check_edges.setdefault(check_indices[id(dependency)], set()).add(check_indices[id(check)]) + else: + requirement_edges.setdefault(dependency_requirement_index, set()).add(source_requirement_index) + + if check_edges: + ordered_checks = cls._topological_order( + checks, + check_edges, + requirement.profile.identifier, + "checks", + ) + requirement.set_checks_order(ordered_checks) + + return cls._topological_order( + requirements, + requirement_edges, + requirements[0].profile.identifier, + "requirements", + ) + + @classmethod + def dependency_closure( + cls, + requirements: list[Requirement], + *, + include_dependencies: bool = True, + ) -> list[Requirement]: + """ + Return selected requirements plus transitive effective dependencies. + + Dependency names are resolved against the composed profile, allowing a + target-local requirement to pull in requirements declared by inherited + profiles. When ``include_dependencies`` is false, the original + selection is returned in dependency-safe order without expanding it. + """ + selected: list[Requirement] = [] + selected_ids: set[int] = set() + queue = list(requirements) + + while queue: + requirement = queue.pop(0) + if id(requirement) in selected_ids: + continue + selected_ids.add(id(requirement)) + selected.append(requirement) + + checks_by_name = cls.effective_check_index(requirement.profile) + for check in requirement.get_checks(): + for dependency_name in check.depends_on: + dependency = cls._resolve_dependency(check, dependency_name, checks_by_name) + dependency_requirement = dependency.requirement + if id(dependency_requirement) in selected_ids: + continue + if not include_dependencies: + raise CheckDependencyError( + f"check {check.name!r} depends on {dependency_name!r}, " + "but its requirement was not selected", + requirement.profile.identifier, + ) + queue.append(dependency_requirement) + + return selected + @dataclass(frozen=True) class SourceSnippet: @@ -531,7 +901,10 @@ def __init__( level: RequirementLevel | None = LevelCollection.REQUIRED, description: str | None = None, hidden: bool | None = None, + *, deactivated: bool = False, + depends_on: tuple[str, ...] | None = None, + extra_check: bool = False, ): self._requirement: Requirement = requirement self._order_number = 0 @@ -540,6 +913,8 @@ def __init__( self._description = description self._hidden = hidden self._deactivated = deactivated + self._depends_on = tuple(depends_on or ()) + self._extra_check = extra_check @property def order_number(self) -> int: @@ -583,11 +958,16 @@ def level(self) -> RequirementLevel: def severity(self) -> Severity: return self.level.severity + @property + def extra_check(self) -> bool: + """Whether this check reports advisory extra_checks instead of conformance issues.""" + return self._extra_check + @property def overridden_by(self) -> list[RequirementCheck]: overridden_by = [] for sibling_profile in self.requirement.profile.siblings: - check = sibling_profile.get_requirement_check(self.name) + check = sibling_profile.get_requirement_check(self.name, self.severity) if check: overridden_by.append(check) return overridden_by @@ -596,7 +976,7 @@ def overridden_by(self) -> list[RequirementCheck]: def overrides(self) -> list[RequirementCheck]: overrides = [] for parent in self.requirement.profile.parents: - check = parent.get_requirement_check(self.name) + check = parent.get_requirement_check(self.name, self.severity) if check: overrides.append(check) return overrides @@ -609,6 +989,11 @@ def overridden(self) -> bool: def deactivated(self) -> bool: return self._deactivated + @property + def depends_on(self) -> tuple[str, ...]: + """Names of checks that must run before this check.""" + return self._depends_on + @property def hidden(self) -> bool: if self._hidden is not None: @@ -616,7 +1001,7 @@ def hidden(self) -> bool: return self.requirement.hidden @abstractmethod - def execute_check(self, context: ValidationContext) -> bool: + def execute_check(self, context: ValidationContext) -> CheckResultValue: raise NotImplementedError() def get_source_snippet(self) -> SourceSnippet | None: @@ -635,6 +1020,8 @@ def to_dict(self, with_requirement: bool = True, with_profile: bool = True) -> d "name": self.name, "description": self.description, "severity": self.severity.name, + "extra_check": self.extra_check, + "depends_on": list(self.depends_on), } if with_requirement: result["requirement"] = self.requirement.to_dict(with_profile=with_profile, with_checks=False) diff --git a/rocrate_validator/models/result.py b/rocrate_validator/models/result.py index 3dcf4979f..da1d4491f 100644 --- a/rocrate_validator/models/result.py +++ b/rocrate_validator/models/result.py @@ -23,6 +23,7 @@ from rocrate_validator import __version__ from rocrate_validator.constants import JSON_OUTPUT_FORMAT_VERSION from rocrate_validator.models._logging import logger +from rocrate_validator.models.check_result import CheckResult, CheckResultValue, normalize_check_result from rocrate_validator.models.requirement import ( Requirement, RequirementCheck, @@ -31,6 +32,12 @@ RequirementLevel, Severity, ) +from rocrate_validator.models.skipped_check import ( + SkipCategory, + SkipCategoryInput, + SkippedCheckDetail, + normalize_skip_category, +) if TYPE_CHECKING: from collections.abc import Collection @@ -54,12 +61,32 @@ def __init__( violatingProperty: str | None = None, violatingEntity: str | None = None, value: str | None = None, + *, + context: ValidationContext | None = None, ): self._message = message self._check: RequirementCheck = check self._violatingProperty = violatingProperty self._violatingEntity = violatingEntity self._propertyValue = value + self._context = context + + @property + def identifier(self) -> str: + """Check identifier as exposed by the active validation target.""" + return self._context.effective_check_identifier(self.check) if self._context else self.check.identifier + + @property + def profile_identifier(self) -> str: + """Effective profile identifier used for reporting.""" + if self._context: + return self._context.effective_check_profile(self.check).identifier + return self.check.requirement.profile.identifier + + @property + def source_profile_identifier(self) -> str: + """Profile that physically declares the check implementation.""" + return self.check.requirement.profile.identifier @property def message(self) -> str | None: @@ -133,7 +160,7 @@ def __repr__(self) -> str: return f"CheckIssue(severity={self.severity}, check={self.check}, message={self.message})" def __str__(self) -> str: - return f'Issue of severity {self.severity.name} with check "{self.check.identifier}": {self.message}' + return f'Issue of severity {self.severity.name} with check "{self.identifier}": {self.message}' def to_dict( self, @@ -150,6 +177,10 @@ def to_dict( } if with_check: result["check"] = self.check.to_dict(with_requirement=with_requirement, with_profile=with_profile) + result["check"]["source_identifier"] = self.check.identifier + result["check"]["identifier"] = self.identifier + result["check"]["source_profile"] = self.source_profile_identifier + result["check"]["profile"] = self.profile_identifier return result def to_json( @@ -169,6 +200,44 @@ def to_json( ) +class ValidationExtraCheck: + """A non-blocking extra_check produced by an optional heuristic check.""" + + def __init__( + self, + check: RequirementCheck, + message: str, + context: ValidationContext, + *, + violating_entity: str | None = None, + violating_property: str | None = None, + value: str | None = None, + ): + self.check = check + self.message = message + self.identifier = context.effective_check_identifier(check) + self.violating_entity = violating_entity + self.violating_property = violating_property + self.value = value + + def to_dict(self) -> dict[str, str | None]: + return { + "check": self.identifier, + "message": self.message, + "violatingEntity": self.violating_entity, + "violatingProperty": self.violating_property, + "violatingPropertyValue": self.value, + } + + def __repr__(self) -> str: + return f"ValidationExtraCheck(check={self.identifier!r}, message={self.message!r})" + + def __eq__(self, other: object) -> bool: + return isinstance(other, ValidationExtraCheck) and self.to_dict() == other.to_dict() + + __hash__ = None # type: ignore[assignment] + + class ValidationResult: """ Represents the result of a validation. @@ -194,13 +263,16 @@ def __init__(self, context: ValidationContext): self._validation_settings: ValidationSettings = context.settings # keep track of the issues found during the validation self._issues: list[CheckIssue] = [] + self._extra_checks: list[ValidationExtraCheck] = [] # keep track of the checks that have been executed self._executed_checks: set[RequirementCheck] = set() self._executed_checks_results: dict[str, bool] = {} + self._check_results: dict[str, CheckResult] = {} # keep track of the checks that have been skipped self._skipped_checks: set[RequirementCheck] = set() + self._skipped_check_details: dict[str, SkippedCheckDetail] = {} # initialize the statistics - self._statistics = ValidationStatistics(context.settings) + self._statistics = ValidationStatistics(context.settings, context=context) @property def context(self) -> ValidationContext: @@ -239,41 +311,104 @@ def executed_checks(self) -> set[RequirementCheck]: """ return self._executed_checks - def _add_executed_check(self, check: RequirementCheck, result: bool): + def _record_check_result( + self, + check: RequirementCheck, + result: CheckResultValue, + skip_message: str | None = None, + skip_category: SkipCategoryInput = SkipCategory.RETURNED, + ) -> CheckResult: + """Record a normalized result and keep check collections consistent.""" + normalized_result = normalize_check_result(result) + self._check_results[check.identifier] = normalized_result + + if normalized_result is CheckResult.SKIPPED: + normalized_skip_category = normalize_skip_category(skip_category) + self._executed_checks.discard(check) + self._skipped_checks.add(check) + self._executed_checks_results.pop(check.identifier, None) + current_detail = self._skipped_check_details.get(check.identifier) + if current_detail is None or (skip_message and current_detail.category == "returned"): + self._skipped_check_details[check.identifier] = SkippedCheckDetail( + check=check, + message=skip_message or "Check returned SKIPPED", + category=normalized_skip_category, + context=getattr(self, "_context", None), + ) + return normalized_result + + self._executed_checks.add(check) + self._skipped_checks.discard(check) + self._skipped_check_details.pop(check.identifier, None) + self._executed_checks_results[check.identifier] = normalized_result is CheckResult.PASSED + return normalized_result + + def _add_executed_check(self, check: RequirementCheck, result: CheckResultValue): """ Internal method to add a check to the executed checks """ - self._executed_checks.add(check) - self._executed_checks_results[check.identifier] = result - # remove the check from the skipped checks if it was skipped - if check in self._skipped_checks: - self._skipped_checks.remove(check) - logger.debug("Removing check '%s' from skipped checks", check.name) + normalized_result = self._record_check_result(check, result) + if normalized_result is not CheckResult.SKIPPED: + logger.debug("Recorded check '%s' as %s", check.name, normalized_result.value) + + def get_check_result(self, check: RequirementCheck) -> CheckResult | None: + """Get the normalized result of a processed check.""" + return self._check_results.get(check.identifier) def get_executed_check_result(self, check: RequirementCheck) -> bool | None: """ - Get the result of an executed check + Get the legacy boolean result of an executed check. + + Skipped checks and checks that have not been processed return ``None``. """ return self._executed_checks_results.get(check.identifier) @property def skipped_checks(self) -> set[RequirementCheck]: """ - The checks that have been skipped + The reportable checks that have been skipped. + + Internal outcomes remain available through ``get_check_result`` even + for hidden, overridden or otherwise out-of-scope checks. """ - return self._skipped_checks + return {check for check in self._skipped_checks if check in self.statistics.checks} + + @property + def skipped_checks_count(self) -> int: + """Get the number of skipped checks.""" + return len(self.skipped_checks) + + @property + def skipped_check_details(self) -> list[SkippedCheckDetail]: + """Get the ordered explanations for skipped checks.""" + return [detail for detail in self._skipped_check_details.values() if detail.check in self.statistics.checks] + + def record_skip( + self, + check: RequirementCheck, + message: str, + category: SkipCategoryInput = SkipCategory.RETURNED, + ) -> None: + """Record a skip message while preserving the normalized skipped result.""" + self._record_check_result(check, CheckResult.SKIPPED, message, category) - def _add_skipped_check(self, check: RequirementCheck): + def _add_skipped_check( + self, + check: RequirementCheck, + message: str = "Check was skipped", + category: SkipCategoryInput = SkipCategory.RETURNED, + ): """ Internal method to add a check to the skipped checks """ - self._skipped_checks.add(check) + self._record_check_result(check, CheckResult.SKIPPED, message, category) def _remove_skipped_check(self, check: RequirementCheck): """ Internal method to remove a check from the skipped checks """ self._skipped_checks.remove(check) + self._skipped_check_details.pop(check.identifier, None) # --- Issues --- @property @@ -283,6 +418,30 @@ def issues(self) -> list[CheckIssue]: """ return self._issues.copy() + @property + def extra_checks(self) -> list[ValidationExtraCheck]: + """Non-blocking heuristic findings, separate from conformance issues.""" + return self._extra_checks.copy() + + def add_extra_check( + self, + check: RequirementCheck, + message: str, + violating_entity: str | None = None, + violating_property: str | None = None, + value: str | None = None, + ) -> ValidationExtraCheck: + extra_check = ValidationExtraCheck( + check, + message, + self.context, + violating_entity=violating_entity, + violating_property=violating_property, + value=value, + ) + self._extra_checks.append(extra_check) + return extra_check + def get_issues(self, min_severity: Severity | None = None) -> list[CheckIssue]: """ Get the issues found during the validation with a severity greater than or equal to `min_severity` @@ -326,16 +485,19 @@ def add_issue( Parameters: message(str): The message of the issue check(RequirementCheck): The check that generated the issue - violatingEntity(Optional[str]): The entity that caused the issue (if any) - violatingProperty(Optional[str]): The property that caused the issue (if any) - violatingPropertyValue(Optional[str]): The value of the violatingProperty (if any) + violatingEntity(Optional[str]): The entity that caused the issue (if any) + violatingProperty(Optional[str]): The property that caused the issue (if any) + violatingPropertyValue(Optional[str]): The value of the violatingProperty (if any) """ + if check.extra_check: + raise ValueError(f"ExtraCheck check '{check.name}' cannot add a conformance issue") c = CheckIssue( check, message, violatingProperty=violatingProperty, violatingEntity=violatingEntity, value=violatingPropertyValue, + context=self.context, ) bisect.insort(self._issues, c) return c @@ -383,9 +545,9 @@ def __repr__(self): def __eq__(self, other: object) -> bool: if not isinstance(other, ValidationResult): raise TypeError(f"Cannot compare ValidationResult with {type(other)}") - return self._issues == other._issues + return self._issues == other._issues and self._extra_checks == other._extra_checks - # Equality is based on the mutable list of issues, so instances are + # Equality is based on mutable issue and extra_check lists, so instances are # intentionally unhashable (a content-based hash would be unstable). __hash__ = None # type: ignore[assignment] @@ -398,15 +560,28 @@ def to_dict(self) -> dict: "enable_profile_inheritance", "requirement_severity", "abort_on_first", + "metadata_only", + "packaging_mode", + "package_root", ] validation_settings = { key: value for key, value in self.validation_settings.to_dict().items() if key in allowed_properties } + validation_settings["package_type"] = self.context.ro_crate.package_type.value + validation_settings["packaging_mode_explicit"] = self.context.ro_crate.packaging_mode_explicit + validation_settings["metadata_source"] = ( + "dictionary" if self.validation_settings.metadata_dict is not None else "package descriptor" + ) + if self.validation_settings.package_root is not None: + validation_settings["package_root"] = str(self.validation_settings.package_root) result: dict[str, Any] = { "meta": {"version": JSON_OUTPUT_FORMAT_VERSION}, "validation_settings": validation_settings, "passed": self.passed(cast("Severity", self.context.settings.requirement_severity)), "issues": [issue.to_dict() for issue in self.issues], + "extra_checks": [extra_check.to_dict() for extra_check in self.extra_checks], + "skipped_checks": self.skipped_checks_count, + "skipped_check_details": [detail.to_dict() for detail in self.skipped_check_details], } # add validator version to the settings result["validation_settings"]["rocrate_validator_version"] = __version__ diff --git a/rocrate_validator/models/settings.py b/rocrate_validator/models/settings.py index da3df7ceb..c0c785d9f 100644 --- a/rocrate_validator/models/settings.py +++ b/rocrate_validator/models/settings.py @@ -23,6 +23,7 @@ ) from rocrate_validator.models._logging import logger from rocrate_validator.models.severity import Severity +from rocrate_validator.rocrate.package_type import parse_packaging_mode from rocrate_validator.utils.cache_warmup import auto_warm_up_for_settings from rocrate_validator.utils.document_loader import install_document_loader from rocrate_validator.utils.http import HttpRequester @@ -47,7 +48,8 @@ class ValidationSettings: """ #: The URI of the RO-Crate - rocrate_uri: URI # pyright: ignore[reportRedeclaration] + rocrate_uri: URI | None # pyright: ignore[reportRedeclaration] + _source_rocrate_uri: URI | None = None #: The relative root path of the RO-Crate rocrate_relative_root_path: Path | None = None # Profile settings @@ -82,14 +84,22 @@ class ValidationSettings: # Requirement check settings #: Flag to allow requirement check override allow_requirement_check_override: bool = True + #: Flag to validate profile consistency checks before validation + validate_profile_checks: bool = True #: Flag to disable the check for duplicates disable_check_for_duplicates: bool = False #: Checks to skip skip_checks: list[str] | None = None + #: Run checks classified as extra_check, without affecting conformance + extra_checks: bool = False #: Flag to validate only the metadata of the RO-Crate metadata_only: bool = False + #: Packaging context: auto, attached, or detached + packaging_mode: str = "auto" #: RO-Crate metadata as dictionary metadata_dict: dict | None = None + #: Local package directory used as the payload source for dictionary metadata + package_root: Path | None = None #: Verbose output verbose: bool = False #: Cache max age in seconds (negative values mean "never expire") @@ -108,6 +118,9 @@ class ValidationSettings: skip_availability_check: bool = False def __post_init__(self): + self.packaging_mode = parse_packaging_mode(self.packaging_mode) + if self.package_root is not None and not isinstance(self.package_root, Path): + self.package_root = Path(self.package_root) # if requirement_severity is a str, convert to Severity if isinstance(self.requirement_severity, str): self.requirement_severity = Severity[self.requirement_severity] @@ -163,7 +176,10 @@ def to_dict(self): Convert the ValidationSettings to a dictionary """ result = asdict(self) - result["rocrate_uri"] = str(self.rocrate_uri) + source_uri = getattr(self, "_source_rocrate_uri", None) + if source_uri is None: + source_uri = self.rocrate_uri + result["rocrate_uri"] = str(source_uri) if source_uri is not None else None result.pop("metadata_dict", None) # exclude metadata_dict from the dict representation # Remove disable_crate_download from the dict representation result.pop("disable_remote_crate_download", None) @@ -182,16 +198,19 @@ def rocrate_uri(self) -> URI | None: return self._rocrate_uri @rocrate_uri.setter - def rocrate_uri(self, value: str | Path | URI): + def rocrate_uri(self, value: str | Path | URI | None): """ Set the RO-Crate URI. :param value: The RO-Crate URI. :type value: Union[str, Path, URI] """ + if value is None or isinstance(value, property): + self._rocrate_uri = None + return if not value: raise ValueError("Invalid RO-Crate URI") - self._rocrate_uri: URI = URI(str(value)) + self._rocrate_uri = URI(str(value)) @classmethod def parse(cls, settings: dict | ValidationSettings) -> ValidationSettings: diff --git a/rocrate_validator/models/skipped_check.py b/rocrate_validator/models/skipped_check.py new file mode 100644 index 000000000..ddb8e80f8 --- /dev/null +++ b/rocrate_validator/models/skipped_check.py @@ -0,0 +1,127 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import Enum, unique +from typing import TYPE_CHECKING, Literal, TypeAlias + +if TYPE_CHECKING: + from rocrate_validator.models.requirement import RequirementCheck + from rocrate_validator.models.validation import ValidationContext + + +@unique +class SkipCategory(str, Enum): + """Reason category for a skipped requirement check.""" + + _description: str + + RETURNED = ("returned", "The check returned SKIPPED without another skip reason.") + CONFIGURED = ("configured", "The check was skipped by validation settings.") + EXCEPTION = ("exception", "The check was skipped because an exception prevented validation.") + DEACTIVATED = ("deactivated", "The check is deactivated in the profile.") + DEPENDENCY = ("dependency", "The check was skipped because a dependency did not pass.") + NOT_REACHED = ("not_reached", "The check was not reached because validation stopped earlier.") + INHERITED = ("inherited", "The check belongs to an inherited profile that was not reported or run.") + + def __new__(cls, value: str, description: str): + member = str.__new__(cls, value) + member._value_ = value + member._description = description + return member + + @property + def description(self) -> str: + """Human-readable explanation of this category.""" + return self._description + + +SkipCategoryInput: TypeAlias = ( + SkipCategory + | Literal[ + "returned", + "configured", + "exception", + "deactivated", + "dependency", + "not_reached", + "inherited", + ] +) + + +def normalize_skip_category(value: SkipCategoryInput) -> SkipCategory: + """Convert a public skip-category value to its enum member.""" + if isinstance(value, SkipCategory): + return value + return { + "returned": SkipCategory.RETURNED, + "configured": SkipCategory.CONFIGURED, + "exception": SkipCategory.EXCEPTION, + "deactivated": SkipCategory.DEACTIVATED, + "dependency": SkipCategory.DEPENDENCY, + "not_reached": SkipCategory.NOT_REACHED, + "inherited": SkipCategory.INHERITED, + }[value] + + +class SkipRequirementCheck(Exception): + """Signal that a requirement check should be skipped.""" + + def __init__( + self, + check: RequirementCheck, + message: str = "", + category: SkipCategoryInput = SkipCategory.RETURNED, + ): + self.check = check + self.message = message + self.category = normalize_skip_category(category) + + def __str__(self) -> str: + return f"SkipRequirementCheck(check={self.check})" + + +@dataclass(frozen=True) +class SkippedCheckDetail: + """Structured information explaining why a check was skipped.""" + + check: RequirementCheck + message: str + category: SkipCategory = SkipCategory.RETURNED + context: ValidationContext | None = field(default=None, compare=False, repr=False) + + def __post_init__(self) -> None: + object.__setattr__(self, "category", normalize_skip_category(self.category)) + + def to_dict(self) -> dict[str, str]: + source_identifier = self.check.identifier + source_profile = self.check.requirement.profile.identifier + identifier = self.context.effective_check_identifier(self.check) if self.context else source_identifier + profile = self.context.effective_check_profile(self.check).identifier if self.context else source_profile + result = { + "identifier": identifier, + "profile": profile, + "requirement": self.check.requirement.name, + "name": self.check.name, + "severity": self.check.severity.name, + "message": self.message, + "category": self.category.value, + } + if identifier != source_identifier or profile != source_profile: + result["source_identifier"] = source_identifier + result["source_profile"] = source_profile + return result diff --git a/rocrate_validator/models/statistics.py b/rocrate_validator/models/statistics.py index c7f7e3536..3c3273157 100644 --- a/rocrate_validator/models/statistics.py +++ b/rocrate_validator/models/statistics.py @@ -20,6 +20,7 @@ from rocrate_validator.events import Event, EventType, Subscriber from rocrate_validator.models._logging import logger +from rocrate_validator.models.check_result import CheckResult, normalize_check_result from rocrate_validator.models.events import ( ProfileValidationEvent, RequirementCheckValidationEvent, @@ -67,7 +68,7 @@ def __init__( settings = ValidationSettings.parse(settings) self._settings = settings self._context = context - self._stats = self.__initialise__(settings) if not skip_initialization else {} + self._stats = self.__initialise__(settings, context=context) if not skip_initialization else {} self._result: ValidationResult | None = None self._listeners: list[ValidationStatisticsListener] = [] @@ -191,6 +192,18 @@ def failed_checks(self) -> list[RequirementCheck]: """ return self._stats.get("failed_checks", []) + @property + def skipped_checks(self) -> list[RequirementCheck]: + """ + Get the list of skipped checks + """ + return self._stats.get("skipped_checks", []) + + @property + def total_skipped_checks(self) -> int: + """Get the number of skipped checks.""" + return len(self.skipped_checks) + @property def total_checks(self) -> int: """ @@ -198,6 +211,25 @@ def total_checks(self) -> int: """ return len(self._stats.get("checks", [])) + @property + def extra_check_checks(self) -> list[RequirementCheck]: + """Get all extra_check checks available to this validation.""" + return self._stats.get("extra_check_checks", []) + + @property + def executed_extra_check_checks(self) -> list[RequirementCheck]: + """Get extra_check checks that ran, regardless of their findings.""" + return self._stats.get("executed_extra_check_checks", []) + + @property + def skipped_extra_check_checks(self) -> list[RequirementCheck]: + """Get extra_check checks that were disabled or skipped.""" + return self._stats.get("skipped_extra_check_checks", []) + + @property + def total_extra_check_checks(self) -> int: + return len(self.extra_check_checks) + @property def validated_profiles(self) -> list[Profile]: """ @@ -219,6 +251,14 @@ def validated_checks(self) -> list[RequirementCheck]: """ return self._stats.get("validated_checks", []) + def effective_check_identifier(self, check: RequirementCheck) -> str: + """Return the identifier used to report a check in this validation.""" + return self._context.effective_check_identifier(check) if self._context else check.identifier + + def effective_check_profile(self, check: RequirementCheck) -> Profile: + """Return the profile used to report a check in this validation.""" + return self._context.effective_check_profile(check) if self._context else check.requirement.profile + @property def started_at(self) -> datetime | None: """ @@ -247,8 +287,8 @@ def duration(self) -> float | None: @staticmethod def __collect_requirement_checks__( requirement, + *, severity_validation, - validation_settings, target_profile_identifier, checks, checks_by_severity, @@ -268,7 +308,7 @@ def __collect_requirement_checks__( requirement_checks = [ _ for _ in requirement.get_checks_by_level(LevelCollection.get(severity.name)) - if (not validation_settings.skip_checks or _.identifier not in validation_settings.skip_checks) + if not _.extra_check and (not _.overridden or _.requirement.profile.identifier == target_profile_identifier) ] num_checks = len(requirement_checks) @@ -280,31 +320,50 @@ def __collect_requirement_checks__( return requirement_checks_count @classmethod - def __initialise__(cls, validation_settings: ValidationSettings): + def __initialise__( + cls, + validation_settings: ValidationSettings, + context: ValidationContext | None = None, + ): # pylint: disable=too-many-locals """ Compute the statistics of the profile """ # extract the validation settings severity_validation = validation_settings.requirement_severity - profiles: list[Profile] = Profile.load_profiles( - validation_settings.profiles_path, - extra_profiles_path=validation_settings.extra_profiles_path, - severity=cast("Severity", severity_validation), - allow_requirement_check_override=validation_settings.allow_requirement_check_override, - ) + profiles: list[Profile] + if context is not None: + # Reuse the exact profile instances selected by the validation + # context. They already carry the lazily prepared requirements and + # shape registries used by the run. + profiles = context.profiles + else: + profiles = Profile.load_profiles( + validation_settings.profiles_path, + extra_profiles_path=validation_settings.extra_profiles_path, + severity=cast("Severity", severity_validation), + allow_requirement_check_override=validation_settings.allow_requirement_check_override, + ) profile: Profile = cast("Profile", Profile.find_in_list(profiles, validation_settings.profile_identifier)) target_profile_identifier = profile.identifier # initialize the profiles list profiles = [profile] # add inherited profiles if enabled - if not validation_settings.disable_inherited_profiles_issue_reporting: - profiles.extend(profile.inherited_profiles) + profiles.extend( + inherited + for inherited in profile.inherited_profiles + if validation_settings.enable_profile_inheritance + and ( + not validation_settings.disable_inherited_profiles_issue_reporting + or inherited.uri in profile.is_rule_overlay_of + ) + ) logger.debug("Inherited profiles: %r", profile.inherited_profiles) # Initialize the counters checks_by_severity: dict[Severity, set[RequirementCheck]] = {} checks: set[RequirementCheck] = set() + extra_check_checks: set[RequirementCheck] = set() requirements: set[Requirement] = set() # Initialize the counters @@ -317,26 +376,36 @@ def __initialise__(cls, validation_settings: ValidationSettings): # Process the requirements and checks processed_requirements = [] - for profile in profiles: - for requirement in profile.requirements: + for validation_profile in profiles: + for requirement in validation_profile.get_requirements( + cast("Severity", severity_validation), exact_match=validation_settings.requirement_severity_only + ): if requirement in processed_requirements: continue processed_requirements.append(requirement) if requirement.hidden: continue + extra_check_checks.update( + check + for check in requirement.get_checks() + if check.extra_check + and (not check.overridden or check.requirement.profile.identifier == target_profile_identifier) + ) + requirement_checks_count = cls.__collect_requirement_checks__( requirement, - severity_validation, - validation_settings, - target_profile_identifier, - checks, - checks_by_severity, + severity_validation=severity_validation, + target_profile_identifier=target_profile_identifier, + checks=checks, + checks_by_severity=checks_by_severity, ) # count the requirements and checks if requirement_checks_count == 0: logger.debug(f"No checks for requirement: {requirement}") + elif requirement.overridden: + logger.debug(f"Requirement is fully overridden and is not counted: {requirement}") else: # Only if there are checks for the requirement count it logger.debug(f"Requirement: {requirement} checks count: {requirement_checks_count}") @@ -357,10 +426,12 @@ def __initialise__(cls, validation_settings: ValidationSettings): "profiles": profiles, "requirements": requirements, "checks": checks, + "extra_check_checks": extra_check_checks, "severity": severity_validation, "checks_by_severity": checks_by_severity, "failed_requirements": [], "failed_checks": [], + "skipped_checks": [], "passed_requirements": [], "passed_checks": [], "started_at": None, @@ -368,6 +439,8 @@ def __initialise__(cls, validation_settings: ValidationSettings): "validated_profiles": [], "validated_requirements": [], "validated_checks": [], + "executed_extra_check_checks": [], + "skipped_extra_check_checks": [], } logger.debug(result) return result @@ -389,37 +462,68 @@ def __handle_requirement_validation_start__(self, _event: Event, _ctx: Validatio def __handle_requirement_check_validation_start__(self, _event: Event, _ctx: ValidationContext | None) -> None: logger.debug("Requirement check validation start") + def __record_check_status__(self, check: RequirementCheck, result: CheckResult) -> None: + """ + Keep one final status for each requirement check. + + A check can emit more than one result while validating a crate (for + example, a dependency may be skipped before a later execution). The + statistics describe the final validation state, not the history of + intermediate events. + """ + for status in ("passed_checks", "failed_checks", "skipped_checks", "validated_checks"): + self._stats[status] = [ + candidate for candidate in self._stats[status] if candidate.identifier != check.identifier + ] + + if result is CheckResult.PASSED: + self._stats["passed_checks"].append(check) + self._stats["validated_checks"].append(check) + elif result is CheckResult.FAILED: + self._stats["failed_checks"].append(check) + self._stats["validated_checks"].append(check) + else: + self._stats["skipped_checks"].append(check) + def __handle_requirement_check_validation_end__(self, event: Event, ctx: ValidationContext | None) -> None: assert isinstance(event, RequirementCheckValidationEvent) assert ctx is not None - target_profile = ctx.target_validation_profile - requirement_severity = self._settings.requirement_severity - if not event.requirement_check.requirement.hidden and ( - not event.requirement_check.overridden - or target_profile.identifier == event.requirement_check.requirement.profile.identifier - ): + if event.requirement_check in self.extra_check_checks: + if event.validation_result is not None: + for key in ("executed_extra_check_checks", "skipped_extra_check_checks"): + self._stats[key] = [ + check for check in self._stats[key] if check.identifier != event.requirement_check.identifier + ] + result = normalize_check_result(event.validation_result) + status = ( + "skipped_extra_check_checks" if result is CheckResult.SKIPPED else "executed_extra_check_checks" + ) + self._stats[status].append(event.requirement_check) + self.notify_listeners() + return + if event.requirement_check in self.checks: if event.validation_result is not None: - if event.requirement_check.severity >= requirement_severity: - if event.validation_result: - self._stats["passed_checks"].append(event.requirement_check) - else: - self._stats["failed_checks"].append(event.requirement_check) - self._stats["validated_checks"].append(event.requirement_check) + result = normalize_check_result(event.validation_result) + self.__record_check_status__(event.requirement_check, result) self.notify_listeners() else: logger.debug( "Requirement check validation result is None: %s", - event.requirement_check.identifier, + event.effective_identifier, ) else: logger.debug( "Skipping requirement check validation: %s", - event.requirement_check.identifier, + event.effective_identifier, ) def __handle_requirement_validation_end__(self, event: Event, _ctx: ValidationContext | None) -> None: assert isinstance(event, RequirementValidationEvent) - if not event.requirement.hidden: + if event.requirement in self.requirements: + for status in ("passed_requirements", "failed_requirements", "validated_requirements"): + self._stats[status] = [ + requirement for requirement in self._stats[status] if requirement != event.requirement + ] if event.validation_result: self._stats["passed_requirements"].append(event.requirement) else: @@ -429,7 +533,8 @@ def __handle_requirement_validation_end__(self, event: Event, _ctx: ValidationCo def __handle_profile_validation_end__(self, event: Event, _ctx: ValidationContext | None) -> None: assert isinstance(event, ProfileValidationEvent) - self._stats["validated_profiles"].append(event.profile) + if event.profile in self.profiles and event.profile not in self.validated_profiles: + self._stats["validated_profiles"].append(event.profile) logger.debug("Profile validation ended: %s", event.profile.identifier) def __handle_validation_end__(self, event: Event, _ctx: ValidationContext | None) -> None: @@ -471,7 +576,15 @@ def to_dict(self) -> dict: "total_checks": self.total_checks, "total_passed_checks": len(self.passed_checks), "total_failed_checks": len(self.failed_checks), + "total_skipped_checks": self.total_skipped_checks, "total_checks_by_severity": {k.name: len(v) for k, v in self.checks_by_severity.items()}, + "extra_check_checks": { + "count": self.total_extra_check_checks, + "executed": len(self.executed_extra_check_checks), + "skipped": len(self.skipped_extra_check_checks), + "findings": len(self.validation_result.extra_checks) if self.validation_result else 0, + "identifiers": sorted(check.identifier for check in self.extra_check_checks), + }, # Requirements involved "requirements": { "count": self.total_requirements, @@ -508,6 +621,13 @@ def to_dict(self) -> dict: "percentage": (len(self.failed_checks) / self.total_checks * 100) if self.total_checks > 0 else 0.0, "identifiers": sorted([c.identifier for c in self.failed_checks]), }, + "skipped": { + "count": self.total_skipped_checks, + "percentage": ( + len(self.skipped_checks) / self.total_checks * 100 if self.total_checks > 0 else 0.0 + ), + "identifiers": sorted([c.identifier for c in self.skipped_checks]), + }, "identifiers": sorted([c.identifier for c in self.checks]), "by_severity": {k.name: len(v) for k, v in self._stats.get("checks_by_severity", {}).items()}, }, @@ -561,7 +681,18 @@ def to_dict(self) -> dict: "total_checks": self.total_checks, "total_passed_checks": len(self.passed_checks), "total_failed_checks": len(self.failed_checks), + "total_skipped_checks": self.total_skipped_checks, "total_checks_by_severity": {k.name: len(v) for k, v in self.checks_by_severity.items()}, + "extra_check_checks": { + "count": sum(stats.total_extra_check_checks for stats in self._statistics_list), + "executed": sum(len(stats.executed_extra_check_checks) for stats in self._statistics_list), + "skipped": sum(len(stats.skipped_extra_check_checks) for stats in self._statistics_list), + "findings": sum( + len(stats.validation_result.extra_checks) + for stats in self._statistics_list + if stats.validation_result is not None + ), + }, # Requirements involved "requirements": { "count": self.total_requirements, @@ -598,6 +729,13 @@ def to_dict(self) -> dict: "percentage": (len(self.failed_checks) / self.total_checks * 100) if self.total_checks > 0 else 0.0, "identifiers": [c.identifier for c in self.failed_checks], }, + "skipped": { + "count": self.total_skipped_checks, + "percentage": ( + len(self.skipped_checks) / self.total_checks * 100 if self.total_checks > 0 else 0.0 + ), + "identifiers": [c.identifier for c in self.skipped_checks], + }, "identifiers": [c.identifier for c in self.checks], }, } @@ -679,6 +817,18 @@ def failed_checks(self) -> set[RequirementCheck]: """ return self._overall_stats.get("failed_checks", set()) + @property + def skipped_checks(self) -> set[RequirementCheck]: + """ + Get the set of skipped checks in the aggregated validation + """ + return self._overall_stats.get("skipped_checks", set()) + + @property + def total_skipped_checks(self) -> int: + """Get the number of skipped checks in the aggregated validation.""" + return len(self.skipped_checks) + @property def started_at(self) -> datetime | None: """ @@ -707,6 +857,7 @@ def __compute_averall_stats__(self): raw_stats = self.__aggregate_raw_stats__(self._statistics_list) return self.__build_sorted_stats_dict__(raw_stats) + # pylint: disable=too-many-locals @classmethod def __aggregate_raw_stats__( cls, @@ -721,6 +872,7 @@ def __aggregate_raw_stats__( checks_by_severity: dict[Severity, set[RequirementCheck]] = {} failed_requirements: set[Requirement] = set() failed_checks: set[RequirementCheck] = set() + skipped_checks: set[RequirementCheck] = set() passed_requirements: set[Requirement] = set() passed_checks: set[RequirementCheck] = set() started_at: datetime | None = None @@ -741,6 +893,7 @@ def __aggregate_raw_stats__( # Aggregate failed and passed requirements and checks failed_requirements.update(stats.failed_requirements) failed_checks.update(stats.failed_checks) + skipped_checks.update(stats.skipped_checks) passed_requirements.update(stats.passed_requirements) passed_checks.update(stats.passed_checks) @@ -763,6 +916,7 @@ def __aggregate_raw_stats__( "checks_by_severity": checks_by_severity, "failed_requirements": failed_requirements, "failed_checks": failed_checks, + "skipped_checks": skipped_checks, "passed_requirements": passed_requirements, "passed_checks": passed_checks, "started_at": started_at, @@ -786,6 +940,7 @@ def __build_sorted_stats_dict__(cls, raw_stats): "checks_by_severity": sorted_checks_by_severity, "failed_requirements": sorted(raw_stats["failed_requirements"], key=lambda r: r.identifier), "failed_checks": sorted(raw_stats["failed_checks"], key=lambda c: c.identifier), + "skipped_checks": sorted(raw_stats["skipped_checks"], key=lambda c: c.identifier), "passed_requirements": sorted(raw_stats["passed_requirements"], key=lambda r: r.identifier), "passed_checks": sorted(raw_stats["passed_checks"], key=lambda c: c.identifier), "started_at": raw_stats["started_at"], diff --git a/rocrate_validator/models/validation.py b/rocrate_validator/models/validation.py index a91a1443f..1b578111a 100644 --- a/rocrate_validator/models/validation.py +++ b/rocrate_validator/models/validation.py @@ -14,6 +14,10 @@ from __future__ import annotations +import copy +import json +import threading +from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Any from urllib.error import HTTPError @@ -21,6 +25,7 @@ from rdflib import Graph from rocrate_validator.errors import ( + CheckDependencyError, ProfileNotFound, ROCrateMetadataNotFoundError, ) @@ -28,23 +33,51 @@ from rocrate_validator.models._logging import logger from rocrate_validator.models.events import ( ProfileValidationEvent, + RequirementCheckValidationEvent, RequirementValidationEvent, ValidationEvent, ) from rocrate_validator.models.profile import Profile +from rocrate_validator.models.profile_detection import detect_rocrate_profiles from rocrate_validator.models.requirement import ( Requirement, + RequirementCheck, RequirementLoader, ) from rocrate_validator.models.result import ValidationResult from rocrate_validator.models.settings import ValidationSettings from rocrate_validator.models.severity import Severity +from rocrate_validator.models.skipped_check import SkipCategory, SkipCategoryInput from rocrate_validator.rocrate import ROCrate from rocrate_validator.utils import log as logging from rocrate_validator.utils.http import find_offline_cache_miss +from rocrate_validator.utils.rdf import PREPARED_PROFILE_BASE, extract_base_from_jsonld, parse_turtle_with_relative_iris +from rocrate_validator.utils.uri import URI if TYPE_CHECKING: - from rocrate_validator.utils.uri import URI + import tempfile + + from rdflib.term import Node + + from rocrate_validator.models.profile_detection import ProfileDetectionResult + from rocrate_validator.models.profile_provenance import EffectiveRequirementCheck + + +@dataclass(frozen=True) +class PreparedValidationPlan: + """ + Profile state reusable by validation runs with compatible RDF bases. + + Profiles own their lazily loaded requirements and shape registries, so keeping + the same profile instances avoids reparsing those artifacts on every run. The + plan deliberately excludes all crate data, results and SHACL execution state; + those remain scoped to :class:`ValidationContext`. + """ + + profiles: tuple[Profile, ...] + ontology_graph: Graph + ontology_relative_iris: frozenset[Node] + resolved_profile_identifier: str class Validator(Publisher): @@ -60,9 +93,13 @@ class Validator(Publisher): Initializes the Validator with the given settings. validation_settings() -> ValidationSettings: Returns the validation settings. - detect_rocrate_profiles() -> list[Profile]: + ``detect_rocrate_profiles(rocrate_uri=None, metadata_dict=None)``: Detects the profiles to validate against. - validate() -> ValidationResult: + ``prepare(rocrate_uri=None, metadata_dict=None, refresh=False)``: + Eagerly prepares reusable profile artifacts. + clear_prepared_profiles(): + Invalidates profile preparation owned by this validator. + ``validate(rocrate_uri=None, metadata_dict=None)``: Validate the RO-Crate against the detected profiles according to the validation settings validate_requirements(requirements: list[Requirement]) -> ValidationResult: Validates the RO-Crate against the specified subset of the profile requirements. @@ -70,90 +107,241 @@ class Validator(Publisher): def __init__(self, settings: dict | ValidationSettings): self._validation_settings = ValidationSettings.parse(settings) + self._extracted_crate_directory: tempfile.TemporaryDirectory | None = None super().__init__() # initialize the current context self.__current_context__: ValidationContext | None = None + # Ordinary profile artifacts are prepared against a stable internal base + # and materialized per run. An explicit JSON-LD @base different from the + # crate public ID remains base-specific. + self.__prepared_profile_catalogs: dict[tuple[Any, ...], tuple[Profile, ...]] = {} + self.__prepared_validation_plans: dict[tuple[Any, ...], PreparedValidationPlan] = {} + # Publisher state and ``__current_context__`` are instance-local and + # mutable. Serialize calls made on the same Validator instead of + # allowing two runs to corrupt each other's event/statistics context. + self.__run_lock = threading.Lock() @property def validation_settings(self) -> ValidationSettings: return self._validation_settings - def detect_rocrate_profiles(self) -> list[Profile]: - """ - Detect the profiles to validate against - """ - try: - # initialize the validation context - context = ValidationContext(self, self.validation_settings) - candidate_profiles_uris: set[str] = set() - try: - candidate_profiles_uris.update(context.ro_crate.metadata.get_conforms_to() or []) - except Exception as e: - logger.debug("Error while getting candidate profiles URIs: %s", e) - try: - candidate_profiles_uris.update(context.ro_crate.metadata.get_root_data_entity_conforms_to() or []) - except Exception as e: - logger.debug("Error while getting candidate profiles URIs: %s", e) - - logger.debug("Candidate profiles: %s", candidate_profiles_uris) - if not candidate_profiles_uris: - logger.debug("Unable to determine the profile to validate against") - return [] - # load the profiles - profiles = [] - candidate_profiles = [] - available_profiles = Profile.load_profiles( - context.profiles_path, - extra_profiles_path=context.extra_profiles_path, - publicID=context.publicID, - severity=context.requirement_severity, + @staticmethod + def __prepared_profile_catalog_key__(context: ValidationContext) -> tuple[Any, ...]: + return ( + context.profiles_path.resolve(), + context.extra_profiles_path.resolve() if context.extra_profiles_path else None, + context.prepared_profile_base, + context.requirement_severity, + context.allow_requirement_check_override, + ) + + def __get_prepared_profile_catalog__(self, context: ValidationContext) -> tuple[Profile, ...]: + key = self.__prepared_profile_catalog_key__(context) + profiles = self.__prepared_profile_catalogs.get(key) + if profiles is None: + profiles = tuple( + Profile.load_profiles( + context.profiles_path, + extra_profiles_path=context.extra_profiles_path, + publicID=context.prepared_profile_base, + severity=context.requirement_severity, + allow_requirement_check_override=context.allow_requirement_check_override, + ) ) - profiles = [p for p in available_profiles if p.uri in candidate_profiles_uris] - # get the candidate profiles + self.__prepared_profile_catalogs[key] = profiles + return profiles + + @staticmethod + def __prepared_validation_plan_key__(context: ValidationContext) -> tuple[Any, ...]: + return ( + context.profiles_path.resolve(), + context.extra_profiles_path.resolve() if context.extra_profiles_path else None, + context.prepared_profile_base, + context.prepared_ontology_base, + context.profile_identifier, + context.requirement_severity, + context.inheritance_enabled, + context.allow_requirement_check_override, + context.disable_check_for_duplicates, + ) + + def __get_prepared_validation_plan__(self, context: ValidationContext) -> PreparedValidationPlan: + key = self.__prepared_validation_plan_key__(context) + plan = self.__prepared_validation_plans.get(key) + if plan is None: + profiles = tuple(context.__load_profiles__()) + resolved_profile_identifier = context.profile_identifier + ontology_graph = Graph() + ontology_relative_iris: set[Node] = set() for profile in profiles: - candidate_profiles.append(profile) - inherited_profiles = profile.inherited_profiles - for inherited_profile in inherited_profiles: - if inherited_profile in candidate_profiles: - candidate_profiles.remove(inherited_profile) - logger.debug( - "%d Candidate Profiles found: %s", - len(candidate_profiles), - candidate_profiles, + # Materialize lazy requirements and their per-profile shape + # registries once. Contextual graphs passed to pySHACL are still + # copied for every validation run. + _ = profile.requirements + ontology_path = profile.path / "ontology.ttl" + if ontology_path.exists(): + profile_ontology_graph, relative_iris = parse_turtle_with_relative_iris( + ontology_path, + context.prepared_ontology_base, + ) + ontology_graph += profile_ontology_graph + ontology_relative_iris.update(relative_iris) + plan = PreparedValidationPlan( + profiles, + ontology_graph, + frozenset(ontology_relative_iris), + resolved_profile_identifier, ) - # unmatched candidate profiles - unmatched_profiles = candidate_profiles_uris.difference({p.uri for p in profiles}) - logger.debug("Unmatched Candidate Profiles URIs: %s", unmatched_profiles) - if len(unmatched_profiles) > 0: - logger.warning( - "The conformance to the following profiles could not be verified: %s", - ", ".join(unmatched_profiles), - ) - return candidate_profiles + self.__prepared_validation_plans[key] = plan + # Loading may resolve a bare profile token (for example + # ``ro-crate``) to a versioned identifier and update the settings. + # Store the canonical key as an alias for subsequent warm runs. + self.__prepared_validation_plans[self.__prepared_validation_plan_key__(context)] = plan + # A per-call input uses a copy of the validator settings. On a cache hit + # that copy still contains a bare token, so restore the resolved target + # profile identifier before SHACL dispatch compares it with each profile. + context.settings.profile_identifier = plan.resolved_profile_identifier + return plan + + def clear_prepared_profiles(self) -> None: + """ + Discard all profile preparation cached by this validator. - except Exception: - if logger.isEnabledFor(logging.DEBUG): - logger.exception("Error detecting RO-Crate profiles") - return [] + Call this after changing profile files on disk. Existing validation + results remain valid and keep their own per-run contexts. + """ - def validate(self) -> ValidationResult: + with self.__run_lock: + self.__prepared_profile_catalogs.clear() + self.__prepared_validation_plans.clear() + + def prepare( + self, + rocrate_uri: str | Path | URI | None = None, + *, + metadata_dict: dict | None = None, + refresh: bool = False, + ) -> None: """ - Validate the RO-Crate against the detected profiles according to the validation settings + Eagerly prepare the selected profile for a later validation run. + + By default an instance prepares profiles lazily during its first + :meth:`validate` call. ``refresh=True`` first discards every catalog and + plan cached by this validator, which is useful after editing profile + files in a long-running process. """ - return self.__do_validate__() - def validate_requirements(self, requirements: list[Requirement]) -> ValidationResult: + settings = self.__settings_for_run__(self.validation_settings, rocrate_uri, metadata_dict) + with self.__run_lock: + if refresh: + self.__prepared_profile_catalogs.clear() + self.__prepared_validation_plans.clear() + context = ValidationContext(self, settings) + _ = context.prepared_validation_plan + + @staticmethod + def __settings_for_run__( + settings: ValidationSettings, + rocrate_uri: str | Path | URI | None, + metadata_dict: dict | None, + ) -> ValidationSettings: + if rocrate_uri is not None and metadata_dict is not None: + raise ValueError("rocrate_uri and metadata_dict are mutually exclusive") + if rocrate_uri is None and metadata_dict is None: + # Preserve the established behavior, including writing a resolved + # bare profile identifier back to the caller's settings. + return settings + + run_settings = copy.copy(settings) + if rocrate_uri is not None: + run_settings.rocrate_uri = URI(str(rocrate_uri)) + run_settings.metadata_dict = None + else: + if not isinstance(metadata_dict, dict): + raise TypeError("metadata_dict must be a dictionary") + run_settings.metadata_dict = copy.deepcopy(metadata_dict) + run_settings.metadata_only = True + return run_settings + + def detect_rocrate_profiles( + self, + rocrate_uri: str | Path | URI | None = None, + *, + metadata_dict: dict | None = None, + ) -> list[Profile]: """ - Validates the RO-Crate against the specified subset of the profile requirements + Detect the profiles to validate against """ - assert all(isinstance(requirement, Requirement) for requirement in requirements), "Invalid requirement type" - # perform the requirements validation - return self.__do_validate__(requirements) + return self.detect_rocrate_profiles_detailed(rocrate_uri, metadata_dict=metadata_dict).profiles + + def detect_rocrate_profiles_detailed( + self, + rocrate_uri: str | Path | URI | None = None, + *, + metadata_dict: dict | None = None, + ) -> ProfileDetectionResult: + """Detect profiles and describe unusable or unresolved profile hints.""" + settings = self.__settings_for_run__(self.validation_settings, rocrate_uri, metadata_dict) + with self.__run_lock: + context = ValidationContext(self, settings) + return detect_rocrate_profiles( + context, + profile_loader=lambda: self.__get_prepared_profile_catalog__(context), + ) + + def validate( + self, + rocrate_uri: str | Path | URI | None = None, + *, + metadata_dict: dict | None = None, + ) -> ValidationResult: + """ + Validate the RO-Crate against the detected profiles according to the validation settings + """ + settings = self.__settings_for_run__(self.validation_settings, rocrate_uri, metadata_dict) + return self.__do_validate__(settings=settings) + + def validate_requirements( + self, + requirements: list[Requirement], + *, + include_dependencies: bool = True, + rocrate_uri: str | Path | URI | None = None, + metadata_dict: dict | None = None, + ) -> ValidationResult: + """ + Validates the RO-Crate against the specified subset of the profile requirements. - def __do_validate__(self, requirements: list[Requirement] | None = None) -> ValidationResult: + By default, requirements containing transitive check dependencies are added to + the selected subset. Set ``include_dependencies`` to ``False`` to require the + caller to provide the complete dependency closure explicitly. + """ + assert all(isinstance(requirement, Requirement) for requirement in requirements), "Invalid requirement type" + resolved_requirements = RequirementLoader.dependency_closure( + requirements, + include_dependencies=include_dependencies, + ) + settings = self.__settings_for_run__(self.validation_settings, rocrate_uri, metadata_dict) + return self.__do_validate__(resolved_requirements, settings=settings) + + def __do_validate__( + self, + requirements: list[Requirement] | None = None, + *, + settings: ValidationSettings, + ) -> ValidationResult: + with self.__run_lock: + return self.__do_validate_locked__(requirements, settings=settings) + + def __do_validate_locked__( # noqa: C901, PLR0912, PLR0915 # pylint: disable=too-many-locals,too-many-branches,too-many-statements + self, + requirements: list[Requirement] | None, + *, + settings: ValidationSettings, + ) -> ValidationResult: # initialize the validation context - context = ValidationContext(self, self.validation_settings) + context = ValidationContext(self, settings) # register the current context self.__current_context__ = context @@ -170,8 +358,14 @@ def __do_validate__(self, requirements: list[Requirement] | None = None) -> Vali # profiles that have not yet been visited. for p in profiles: _ = p.requirements + # Requirement objects belong to one prepared plan. Match by stable + # identifier so a caller can reuse a selection when a per-call + # crate input selects an equivalent plan for another RDF base. + selected_requirement_ids = ( + None if requirements is None else {requirement.identifier for requirement in requirements} + ) self.notify(EventType.VALIDATION_START) - for profile in profiles: + for profile_index, profile in enumerate(profiles): logger.debug( "Validating profile %s (id: %s)", profile.name, @@ -181,23 +375,32 @@ def __do_validate__(self, requirements: list[Requirement] | None = None) -> Vali context._target_validation_profile = profile self.notify(ProfileValidationEvent(EventType.PROFILE_VALIDATION_START, profile=profile)) # perform the requirements validation - requirements = profile.get_requirements( - context.requirement_severity, - exact_match=context.requirement_severity_only, - ) + if selected_requirement_ids is None: + profile_requirements = profile.get_requirements( + context.requirement_severity, + exact_match=context.requirement_severity_only, + ) + else: + profile_requirements = [ + requirement + for requirement in profile.requirements + if requirement.identifier in selected_requirement_ids + ] logger.debug( "Validating profile %s with %s requirements", profile.identifier, - len(requirements), + len(profile_requirements), ) logger.debug( "For profile %s, validating these %s requirements: %s", profile.identifier, - len(requirements), - requirements, + len(profile_requirements), + profile_requirements, ) terminate = False - for requirement in requirements: + requirement_index = -1 + for requirement_index in range(len(profile_requirements)): # pylint: disable=consider-using-enumerate + requirement = profile_requirements[requirement_index] if not requirement.overridden: self.notify( RequirementValidationEvent( @@ -235,6 +438,20 @@ def __do_validate__(self, requirements: list[Requirement] | None = None) -> Vali break self.notify(ProfileValidationEvent(EventType.PROFILE_VALIDATION_END, profile=profile)) if terminate: + Requirement.record_skipped_checks(profile_requirements[requirement_index + 1 :], context) + for remaining_profile in profiles[profile_index + 1 :]: + if selected_requirement_ids is None: + remaining_requirements = remaining_profile.get_requirements( + context.requirement_severity, + exact_match=context.requirement_severity_only, + ) + else: + remaining_requirements = [ + requirement + for requirement in remaining_profile.requirements + if requirement.identifier in selected_requirement_ids + ] + Requirement.record_skipped_checks(remaining_requirements, context) break # finalize the requirement types @@ -267,6 +484,11 @@ def notify(self, event: Event | EventType, ctx: Any | None = None): result: ValidationResult = self.__current_context__.result if isinstance(event, EventType): event = Event(event) + if isinstance(event, RequirementCheckValidationEvent): + event.set_effective_identity( + self.__current_context__.effective_check_identifier(event.requirement_check), + self.__current_context__.effective_check_profile(event.requirement_check).identifier, + ) result.statistics.update(event, ctx=self.__current_context__) return super().notify(event, ctx=self.__current_context__) @@ -285,6 +507,9 @@ def __init__(self, validator: Validator, settings: ValidationSettings): self._data_graph: Graph | None = None # reference to the profiles self._profiles: list[Profile] | None = None + # reference to the immutable profile preparation selected for this run + self._prepared_validation_plan: PreparedValidationPlan | None = None + self._effective_ontology_base: str | None = None # reference to the target profile self._target_validation_profile: Profile | None = None # reference to the validation result @@ -293,20 +518,30 @@ def __init__(self, validator: Validator, settings: ValidationSettings): self._properties: dict = {} # URLs already reported as missing from the HTTP cache during this run self._offline_cache_misses_warned: set[str] = set() + # Effective identities are requested by several reporting consumers + # for the same check during one validation run. + self._effective_check_cache: dict[tuple[int, int], EffectiveRequirementCheck] = {} # flag set when the validation must be aborted because the metadata # cannot be read (e.g. the file descriptor is not valid JSON) self._aborted: bool = False self._abort_reason: str | None = None # initialize the ROCrate object - if settings.metadata_dict: - self._rocrate = ROCrate.from_metadata_dict(settings.metadata_dict) + if settings.metadata_dict is not None: + self._rocrate = ROCrate.from_metadata_dict( + settings.metadata_dict, + packaging_mode=settings.packaging_mode, + package_root=settings.package_root, + metadata_only=settings.metadata_only, + ) else: rocrate_uri = settings.rocrate_uri assert rocrate_uri is not None, "RO-Crate URI is required when metadata_dict is not provided" self._rocrate = ROCrate.new_instance( rocrate_uri, relative_root_path=settings.rocrate_relative_root_path, + packaging_mode=settings.packaging_mode, + metadata_only=settings.metadata_only, ) assert isinstance(self._rocrate, ROCrate), "Invalid RO-Crate instance" @@ -342,6 +577,15 @@ def result(self) -> ValidationResult: self._result = ValidationResult(self) return self._result + def record_skip( + self, + check: RequirementCheck, + message: str, + category: SkipCategoryInput = SkipCategory.RETURNED, + ) -> None: + """Record a structured reason while a check is returning ``SKIPPED``.""" + self.result.record_skip(check, message, category) + @property def settings(self) -> ValidationSettings: """ @@ -357,11 +601,54 @@ def publicID(self) -> str: """ The root URI of the RO-Crate """ + if getattr(self.ro_crate, "is_in_memory", False): + return "https://example.invalid/rocrate-validator/in-memory/" path = str(self.ro_crate.uri.base_uri) if not path.endswith("/"): path = f"{path}/" return path + @property + def effective_ontology_base(self) -> str: + """Base used while parsing profile ontology artifacts for this run.""" + + if self._effective_ontology_base is not None: + return self._effective_ontology_base + try: + self._effective_ontology_base = extract_base_from_jsonld(self.ro_crate.metadata.as_dict()) or self.publicID + except (ROCrateMetadataNotFoundError, json.JSONDecodeError, UnicodeDecodeError) as error: + # Profile preparation happens before descriptor checks execute. An + # unreadable descriptor is validation input, not a preparation + # failure: use the crate public ID and let the owning checks report + # the precise missing/JSON/encoding issue in the normal pipeline. + logger.debug("Unable to read metadata @base while preparing profiles: %s", error) + self._effective_ontology_base = self.publicID + return self._effective_ontology_base + + @property + def prepared_profile_base(self) -> str: + """Stable RDF base used to prepare reusable SHACL profile artifacts.""" + + if self.effective_ontology_base == self.publicID: + return PREPARED_PROFILE_BASE + return self.publicID + + @property + def prepared_ontology_base(self) -> str: + """Stable RDF base preserving whether ontology and crate bases differ.""" + + if self.effective_ontology_base == self.publicID: + return self.prepared_profile_base + return self.effective_ontology_base + + @property + def prepared_base_mappings(self) -> tuple[tuple[str, str], ...]: + """Real-to-canonical base mappings for the per-run SHACL data view.""" + + mappings = {self.publicID: self.prepared_profile_base} + mappings[self.effective_ontology_base] = self.prepared_ontology_base + return tuple(sorted(mappings.items(), key=lambda item: len(item[0]), reverse=True)) + @property def profiles_path(self) -> Path: """ @@ -423,7 +710,7 @@ def rocrate_uri(self) -> URI: """ rocrate_uri = self.settings.rocrate_uri if rocrate_uri is None: - raise ValueError("RO-Crate URI is not set") + return self.ro_crate.uri return rocrate_uri @property @@ -570,46 +857,49 @@ def disable_check_for_duplicates(self) -> bool: def __load_profiles__(self) -> list[Profile]: # load all profiles - profiles = Profile.load_profiles( - self.profiles_path, - extra_profiles_path=self.settings.extra_profiles_path, - publicID=self.publicID, - severity=self.requirement_severity, - allow_requirement_check_override=self.allow_requirement_check_override, - ) + profiles = list(self.validator.__get_prepared_profile_catalog__(self)) - # Check if the target profile is in the list of profiles - profile = Profile.get_by_identifier(self.profile_identifier) - if not profile: - try: - candidate_profiles = Profile.get_by_token(self.profile_identifier) - logger.debug("Candidate profiles found by token: %s", profile) - if candidate_profiles: - # Find the profile with the highest version number - profile = max(candidate_profiles, key=lambda p: p.version or "") - self.settings.profile_identifier = profile.identifier - logger.debug("Profile with the highest version number: %s", profile) - except AttributeError as e: - # raised when the profile is not found - if logger.isEnabledFor(logging.DEBUG): - logger.exception("Profile not found: %s", self.profile_identifier) - raise ProfileNotFound( - self.profile_identifier, - message=f"Profile '{self.profile_identifier}' not found in '{self.profiles_path}'", - ) from e - if profile is None: - raise ProfileNotFound( - self.profile_identifier, - message=f"Profile '{self.profile_identifier}' not found in '{self.profiles_path}'", - ) + # Check if the target profile is in the list of profiles. A bare token + # (e.g. `ro-crate`) resolves to the highest available version. + try: + profile = Profile.resolve_in_list(profiles, self.profile_identifier) + except AttributeError as e: + # raised when the profile is not found + if logger.isEnabledFor(logging.DEBUG): + logger.exception("Profile not found: %s", self.profile_identifier) + raise ProfileNotFound( + self.profile_identifier, + message=f"Profile '{self.profile_identifier}' not found in '{self.profiles_path}'", + ) from e + if profile is None: + raise ProfileNotFound( + self.profile_identifier, + message=f"Profile '{self.profile_identifier}' not found in '{self.profiles_path}'", + ) + # Record the resolved identifier, so that downstream consumers (e.g. the + # statistics) agree on which profile was actually used. + if profile.identifier != self.profile_identifier: + logger.debug("Profile %r resolved to %r", self.profile_identifier, profile.identifier) + self.settings.profile_identifier = profile.identifier # if the inheritance is enabled, return only the target profile if not self.inheritance_enabled: return [profile] - # Set the profiles to validate against as the target profile and its inherited profiles + # Visit profiles from the most general source to the target. This is + # important for both dependency ordering and SHACL composition: source + # shapes are collected before the target profile performs the single + # validation run over the complete merged shapes graph. Python checks + # that are replaced by an overlay are dispatched at their source slot. profiles = [*profile.inherited_profiles, profile] + # Validate check identities only for profiles participating in this run. + # Profile listing and discovery may still inspect intentionally invalid + # fixtures without forcing requirement loading. + if self.settings.validate_profile_checks: + for validation_profile in profiles: + validation_profile.validate_profile_checks() + # if the check for duplicates is disabled, return the profiles if self.disable_check_for_duplicates: return profiles @@ -626,9 +916,19 @@ def profiles(self) -> list[Profile]: :rtype: list[Profile] """ if not self._profiles: - self._profiles = self.__load_profiles__() + self._prepared_validation_plan = self.validator.__get_prepared_validation_plan__(self) + self._profiles = list(self._prepared_validation_plan.profiles) return self._profiles.copy() + @property + def prepared_validation_plan(self) -> PreparedValidationPlan: + """Prepared profile state selected for this validation context.""" + + if self._prepared_validation_plan is None: + _ = self.profiles + assert self._prepared_validation_plan is not None + return self._prepared_validation_plan + @property def target_validation_profile(self) -> Profile: """ @@ -650,7 +950,90 @@ def target_profile(self) -> Profile: """ profiles = self.profiles assert len(profiles) > 0, "No profiles to validate" - return self.profiles[-1] + # Overlay validation visits the target before its sources so that + # target-specific fail-fast checks run first; resolve by identifier + # instead of relying on the order of the profiles list. + target = next((profile for profile in profiles if profile.identifier == self.profile_identifier), None) + return target or profiles[-1] + + def is_rule_overlay_source(self, profile: Profile) -> bool: + """Return whether ``profile`` is composed into the validation target.""" + target = self.target_profile + return profile == target or profile.uri in target.is_rule_overlay_of + + def effective_check_identifier(self, check: RequirementCheck) -> str: + """Return a context-local identifier without mutating the source check.""" + return self.effective_check_provenance(check).identifier + + def effective_check_profile(self, check: RequirementCheck) -> Profile: + """Return the reporting profile for a check in this validation.""" + return self.effective_check_provenance(check).profile + + def effective_check_provenance(self, check: RequirementCheck) -> EffectiveRequirementCheck: + """Return and cache the effective identity of ``check`` in this context.""" + target_profile = self.target_profile + cache_key = (id(target_profile), id(check)) + provenance = self._effective_check_cache.get(cache_key) + if provenance is None: + provenance = target_profile.effective_requirement_check(check) + self._effective_check_cache[cache_key] = provenance + return provenance + + def is_check_skipped(self, check: RequirementCheck) -> bool: + """ + Return whether validation settings skip ``check`` under a supported identity. + + A check contributed by an overlay source retains its source identifier + while exposing a target-local effective identifier. Accepting either + identity lets callers address the check without mutating its provenance. + """ + if check.extra_check and not self.settings.extra_checks: + return True + skipped_identifiers = self.settings.skip_checks + if not skipped_identifiers: + return False + check_identifiers = {check.identifier, self.effective_check_identifier(check)} + return bool(check_identifiers.intersection(skipped_identifiers)) + + def check_skip_reason(self, check: RequirementCheck) -> str: + """Describe why validation settings exclude a check.""" + if check.extra_check and not self.settings.extra_checks: + return "extra_check checks are disabled; use --extra-checks to enable them" + return "Check was skipped by validation settings" + + def effective_check_replacement(self, check: RequirementCheck) -> RequirementCheck | None: + """ + Return the target-local check replacing ``check``, if any. + + Replacement is resolved against the effective target profile rather + than the source profile's sibling list. This keeps execution scoped + to the profile selected for the current validation and avoids treating + unrelated profiles loaded in the same registry as active overrides. + """ + target_profile = self.target_profile + if check.requirement.profile == target_profile: + return None + candidate = target_profile.get_requirement_check(check.name, check.severity) + if candidate is not None and check in candidate.overrides: + return candidate + return None + + def resolve_dependency_check(self, check: RequirementCheck, dependency_name: str) -> RequirementCheck: + """ + Resolve a named dependency in the active overlay composition. + + Resolution uses the target profile's effective check set, which includes + inherited checks while excluding definitions shadowed by a more specific + replacement. Exactly one effective match is required; an absent or + ambiguous dependency is a profile-definition error. + """ + matches = RequirementLoader.effective_check_index(self.target_profile).get(dependency_name, []) + if len(matches) != 1: + raise CheckDependencyError( + f"check {check.name!r} depends on unknown or ambiguous check {dependency_name!r}", + check.requirement.profile.identifier, + ) + return matches[0] def get_profile_by_token(self, token: str) -> list[Profile]: """ diff --git a/rocrate_validator/profiles/five-safes-crate/10_outputs.ttl b/rocrate_validator/profiles/five-safes-crate/10_outputs.ttl index 3e3dbb5e3..7d71eb30d 100644 --- a/rocrate_validator/profiles/five-safes-crate/10_outputs.ttl +++ b/rocrate_validator/profiles/five-safes-crate/10_outputs.ttl @@ -30,7 +30,7 @@ five-safes-crate:CreateActionHasResultIfActionCompleted a sh:NodeShape ; - sh:name "WorkflowRunAction" ; + sh:name "Completed workflow run SHOULD have a result" ; sh:description "The `CreateAction` corresponding to the workflow run, with CompletedActionStatus, SHOULD have the `schema:result` property." ; sh:target [ diff --git a/rocrate_validator/profiles/five-safes-crate/11_workflow_execution_phase.ttl b/rocrate_validator/profiles/five-safes-crate/11_workflow_execution_phase.ttl index c5b813e26..bc750b2ef 100644 --- a/rocrate_validator/profiles/five-safes-crate/11_workflow_execution_phase.ttl +++ b/rocrate_validator/profiles/five-safes-crate/11_workflow_execution_phase.ttl @@ -27,12 +27,12 @@ five-safes-crate:WorkflowMustHaveDescriptiveName a sh:NodeShape ; - sh:name "WorkflowExecution" ; + sh:name "Workflow execution MUST have a descriptive name" ; sh:targetClass ro-crate:WorkflowRunAction ; sh:property [ a sh:PropertyShape ; - sh:name "name" ; + sh:name "Workflow execution name" ; sh:minCount 1 ; sh:description "The `CreateAction` corresponding to the workflow run MUST have a name string of at least 10 characters." ; sh:path schema:name ; @@ -45,12 +45,12 @@ five-safes-crate:WorkflowMustHaveDescriptiveName five-safes-crate:WorkflowMustHaveActionStatusWithAllowedValues a sh:NodeShape ; - sh:name "WorkflowExecution" ; + sh:name "Workflow execution MUST have an allowed action status" ; sh:targetClass ro-crate:WorkflowRunAction ; sh:property [ a sh:PropertyShape ; sh:minCount 1 ; - sh:name "actionStatus" ; + sh:name "Workflow execution action status" ; sh:description "`CreateAction` MUST have an actionStatus with an allowed value (see https://schema.org/ActionStatusType)." ; sh:path schema:actionStatus ; sh:in ( @@ -91,7 +91,7 @@ five-safes-crate:WorkflowexecutionObjectHasEndTimeIfEnded sh:property [ a sh:PropertyShape ; - sh:name "EndTime" ; + sh:name "Workflow execution end time" ; sh:path schema:endTime ; sh:minCount 1 ; sh:maxCount 1 ; @@ -132,7 +132,7 @@ five-safes-crate:WorkflowexecutionObjectHasStartTimeIfBegun sh:property [ a sh:PropertyShape ; - sh:name "StartTime" ; + sh:name "Workflow execution start time" ; sh:path schema:startTime ; sh:minCount 1 ; sh:maxCount 1 ; diff --git a/rocrate_validator/profiles/five-safes-crate/12_check_phase.ttl b/rocrate_validator/profiles/five-safes-crate/12_check_phase.ttl index fc4d100f5..41f127ef9 100644 --- a/rocrate_validator/profiles/five-safes-crate/12_check_phase.ttl +++ b/rocrate_validator/profiles/five-safes-crate/12_check_phase.ttl @@ -27,7 +27,7 @@ five-safes-crate:CheckValueObjectHasDescriptiveNameAndIsAssessAction a sh:NodeShape ; - sh:name "CheckValue" ; + sh:name "CheckValue MUST have a name and AssessAction type" ; sh:description "" ; sh:target [ @@ -53,7 +53,7 @@ five-safes-crate:CheckValueObjectHasDescriptiveNameAndIsAssessAction sh:property [ sh:a sh:PropertyShape ; - sh:name "name" ; + sh:name "CheckValue name" ; sh:description "CheckValue MUST have a human readable name string." ; sh:path schema:name ; sh:datatype xsd:string ; @@ -63,7 +63,7 @@ five-safes-crate:CheckValueObjectHasDescriptiveNameAndIsAssessAction five-safes-crate:CheckValueActionStatusMustHaveAllowedValues a sh:NodeShape ; - sh:name "CheckValue" ; + sh:name "CheckValue MUST have an allowed action status" ; sh:description "" ; sh:target [ @@ -82,7 +82,7 @@ five-safes-crate:CheckValueActionStatusMustHaveAllowedValues sh:property [ a sh:PropertyShape ; - sh:name "ActionStatus" ; + sh:name "CheckValue action status" ; sh:path schema:actionStatus ; sh:in ( "http://schema.org/PotentialActionStatus" @@ -99,7 +99,7 @@ five-safes-crate:CheckValueActionStatusMustHaveAllowedValues five-safes-crate:RootDataEntityShouldMentionCheckValueObject a sh:NodeShape ; - sh:name "RootDataEntity" ; + sh:name "Root data entity SHOULD mention CheckValue" ; sh:targetClass ro-crate:RootDataEntity ; sh:description "" ; sh:severity sh:Warning ; @@ -125,7 +125,7 @@ five-safes-crate:RootDataEntityShouldMentionCheckValueObject five-safes-crate:CheckValueObjectShouldPointToRootDataEntity a sh:NodeShape ; - sh:name "CheckValue" ; + sh:name "CheckValue SHOULD reference the root data entity" ; sh:description "" ; sh:target [ a sh:SPARQLTarget ; @@ -141,7 +141,7 @@ five-safes-crate:CheckValueObjectShouldPointToRootDataEntity sh:property [ a sh:PropertyShape ; - sh:name "object" ; + sh:name "CheckValue object" ; sh:path schema:object ; sh:minCount 1 ; sh:class ro-crate:RootDataEntity ; @@ -152,7 +152,7 @@ five-safes-crate:CheckValueObjectShouldPointToRootDataEntity five-safes-crate:CheckValueInstrumentShouldPointToEntityTypedDefinedTerm a sh:NodeShape ; - sh:name "CheckValue" ; + sh:name "CheckValue SHOULD reference a DefinedTerm instrument" ; sh:description "" ; sh:target [ a sh:SPARQLTarget ; @@ -168,7 +168,7 @@ five-safes-crate:CheckValueInstrumentShouldPointToEntityTypedDefinedTerm sh:property [ a sh:PropertyShape ; - sh:name "instrument" ; + sh:name "CheckValue instrument" ; sh:path schema:instrument ; sh:minCount 1 ; sh:class schema:DefinedTerm ; @@ -179,7 +179,7 @@ five-safes-crate:CheckValueInstrumentShouldPointToEntityTypedDefinedTerm five-safes-crate:CheckValueAgentShouldIdentifyTheAgentWhoPerformnedTheCheck a sh:NodeShape ; - sh:name "CheckValue" ; + sh:name "CheckValue SHOULD identify the agent performing the check" ; sh:description "" ; sh:target [ a sh:SPARQLTarget ; @@ -206,7 +206,7 @@ five-safes-crate:CheckValueAgentShouldIdentifyTheAgentWhoPerformnedTheCheck five-safes-crate:CheckValueShouldHaveEndTime a sh:NodeShape ; - sh:name "CheckValue" ; + sh:name "CheckValue SHOULD have an end time" ; sh:description "" ; sh:target [ a sh:SPARQLTarget ; @@ -227,7 +227,7 @@ five-safes-crate:CheckValueShouldHaveEndTime sh:property [ a sh:PropertyShape ; - sh:name "EndTime" ; + sh:name "CheckValue end time" ; sh:path schema:endTime ; sh:minCount 1 ; sh:maxCount 1 ; @@ -238,7 +238,7 @@ five-safes-crate:CheckValueShouldHaveEndTime five-safes-crate:CheckValueShouldHaveActionStatus a sh:NodeShape ; - sh:name "CheckValue" ; + sh:name "CheckValue SHOULD have an action status" ; sh:description "" ; sh:target [ @@ -256,7 +256,7 @@ five-safes-crate:CheckValueShouldHaveActionStatus sh:property [ a sh:PropertyShape ; - sh:name "ActionStatus" ; + sh:name "CheckValue action status" ; sh:path schema:actionStatus ; sh:minCount 1 ; sh:severity sh:Warning ; @@ -290,7 +290,7 @@ five-safes-crate:CheckValueMayHaveStartTime sh:property [ a sh:PropertyShape ; - sh:name "StartTime" ; + sh:name "CheckValue start time" ; sh:path schema:startTime ; sh:minCount 1 ; sh:maxCount 1 ; diff --git a/rocrate_validator/profiles/five-safes-crate/13_validation_phase.ttl b/rocrate_validator/profiles/five-safes-crate/13_validation_phase.ttl index e9d25d46b..e269e3cad 100644 --- a/rocrate_validator/profiles/five-safes-crate/13_validation_phase.ttl +++ b/rocrate_validator/profiles/five-safes-crate/13_validation_phase.ttl @@ -27,7 +27,7 @@ five-safes-crate:ValidationCheckObjectHasDescriptiveNameAndIsAssessAction a sh:NodeShape ; - sh:name "ValidationCheck" ; + sh:name "ValidationCheck MUST have a name and AssessAction type" ; sh:description "" ; sh:target [ @@ -53,7 +53,7 @@ five-safes-crate:ValidationCheckObjectHasDescriptiveNameAndIsAssessAction sh:property [ sh:a sh:PropertyShape ; - sh:name "name" ; + sh:name "ValidationCheck name" ; sh:description "ValidationCheck MUST have a human readable name string." ; sh:path schema:name ; sh:datatype xsd:string ; @@ -64,7 +64,7 @@ five-safes-crate:ValidationCheckObjectHasDescriptiveNameAndIsAssessAction five-safes-crate:ValidationCheckActionStatusMustHaveAllowedValue a sh:NodeShape ; - sh:name "ValidationCheck" ; + sh:name "ValidationCheck MUST have an allowed action status" ; sh:target [ a sh:SPARQLTarget ; sh:select """ @@ -81,7 +81,7 @@ five-safes-crate:ValidationCheckActionStatusMustHaveAllowedValue sh:property [ a sh:PropertyShape ; - sh:name "actionStatus" ; + sh:name "ValidationCheck action status value" ; sh:description "The `actionStatus` of ValidationCheck MUST have an allowed value (see https://schema.org/ActionStatusType)." ; sh:path schema:actionStatus ; sh:in ( @@ -97,7 +97,7 @@ five-safes-crate:ValidationCheckActionStatusMustHaveAllowedValue five-safes-crate:ValidationCheckActionStatusMustHaveAllowedValue a sh:NodeShape ; - sh:name "ValidationCheck" ; + sh:name "ValidationCheck MUST have an allowed action status" ; sh:target [ a sh:SPARQLTarget ; sh:select """ @@ -114,7 +114,7 @@ five-safes-crate:ValidationCheckActionStatusMustHaveAllowedValue sh:property [ a sh:PropertyShape ; sh:minCount 1 ; - sh:name "actionStatus" ; + sh:name "ValidationCheck action status presence and value" ; sh:description "actionStatus MUST be either PotentialActionStatus, ActiveActionStatus, CompletedActionStatus, or FailedActionStatus." ; sh:path schema:actionStatus ; sh:in ( @@ -132,7 +132,7 @@ five-safes-crate:ValidationCheckActionStatusMustHaveAllowedValue five-safes-crate:RootDataEntityShouldMentionValidationCheckObject a sh:NodeShape ; - sh:name "RootDataEntity" ; + sh:name "Root data entity SHOULD mention ValidationCheck" ; sh:targetClass ro-crate:RootDataEntity ; sh:description "" ; sh:severity sh:Warning ; @@ -158,7 +158,7 @@ five-safes-crate:RootDataEntityShouldMentionValidationCheckObject five-safes-crate:ValidationCheckObjectShouldPointToRootDataEntity a sh:NodeShape ; - sh:name "ValidationCheck" ; + sh:name "ValidationCheck SHOULD reference the root data entity" ; sh:description "" ; sh:target [ a sh:SPARQLTarget ; @@ -174,7 +174,7 @@ five-safes-crate:ValidationCheckObjectShouldPointToRootDataEntity sh:property [ a sh:PropertyShape ; - sh:name "object" ; + sh:name "ValidationCheck object" ; sh:path schema:object ; sh:minCount 1 ; sh:class ro-crate:RootDataEntity ; @@ -184,7 +184,7 @@ five-safes-crate:ValidationCheckObjectShouldPointToRootDataEntity five-safes-crate:ValidationCheckInstrumentShouldPointToEntityWithSpecificId a sh:NodeShape ; - sh:name "ValidationCheck" ; + sh:name "ValidationCheck SHOULD reference the Five Safes profile as instrument" ; sh:description "" ; sh:target [ a sh:SPARQLTarget ; @@ -200,7 +200,7 @@ five-safes-crate:ValidationCheckInstrumentShouldPointToEntityWithSpecificId sh:property [ a sh:PropertyShape ; - sh:name "instrument" ; + sh:name "ValidationCheck instrument" ; sh:description "" ; sh:path schema:instrument ; sh:minCount 1 ; @@ -213,7 +213,7 @@ five-safes-crate:ValidationCheckInstrumentShouldPointToEntityWithSpecificId five-safes-crate:ValidationCheckShouldHaveActionStatus a sh:NodeShape ; - sh:name "ValidationCheck" ; + sh:name "ValidationCheck SHOULD have an action status" ; sh:description "" ; sh:target [ @@ -231,7 +231,7 @@ five-safes-crate:ValidationCheckShouldHaveActionStatus sh:property [ a sh:PropertyShape ; - sh:name "ActionStatus" ; + sh:name "ValidationCheck action status" ; sh:path schema:actionStatus ; sh:minCount 1 ; sh:severity sh:Warning ; @@ -241,7 +241,7 @@ five-safes-crate:ValidationCheckShouldHaveActionStatus five-safes-crate:DownloadActionShouldHaveEndTimeIfBegun a sh:NodeShape ; - sh:name "ValidationCheck" ; + sh:name "ValidationCheck SHOULD have an end time when ended" ; sh:description "" ; sh:target [ a sh:SPARQLTarget ; @@ -262,7 +262,7 @@ five-safes-crate:DownloadActionShouldHaveEndTimeIfBegun sh:property [ a sh:PropertyShape ; - sh:name "EndTime" ; + sh:name "ValidationCheck end time" ; sh:path schema:endTime ; sh:minCount 1 ; sh:maxCount 1 ; @@ -298,7 +298,7 @@ five-safes-crate:DownloadActionShouldHaveEndTimeIfBegun sh:property [ a sh:PropertyShape ; - sh:name "StartTime" ; + sh:name "ValidationCheck start time" ; sh:path schema:startTime ; sh:minCount 1 ; sh:maxCount 1 ; diff --git a/rocrate_validator/profiles/five-safes-crate/14_workflow_retrieval_phase.ttl b/rocrate_validator/profiles/five-safes-crate/14_workflow_retrieval_phase.ttl index df7ad33e8..988e3efbf 100644 --- a/rocrate_validator/profiles/five-safes-crate/14_workflow_retrieval_phase.ttl +++ b/rocrate_validator/profiles/five-safes-crate/14_workflow_retrieval_phase.ttl @@ -27,13 +27,13 @@ five-safes-crate:DownloadActionObjectMUSTHavesDescriptiveName a sh:NodeShape ; - sh:name "DownloadAction" ; + sh:name "DownloadAction MUST have a descriptive name" ; sh:targetClass schema:DownloadAction ; sh:description "" ; sh:property [ a sh:PropertyShape ; - sh:name "name" ; + sh:name "DownloadAction name" ; sh:description "DownloadAction MUST have a human readable name string." ; sh:path schema:name ; sh:minCount 1 ; @@ -47,7 +47,7 @@ five-safes-crate:DownloadActionObjectMUSTHavesDescriptiveName five-safes-crate:WorkflowSameAsAndRootDataEntityMainEntityMUSTBeTheSame a sh:NodeShape ; - sh:name "Downloaded Workflow" ; + sh:name "Downloaded workflow sameAs MUST match the root mainEntity" ; sh:description "" ; sh:severity sh:Violation ; @@ -89,7 +89,7 @@ five-safes-crate:WorkflowSameAsAndRootDataEntityMainEntityMUSTBeTheSame five-safes-crate:DownloadedWorkflowDistributionAndDownloadActionObjectMUSTBeTheSame a sh:NodeShape ; - sh:name "Downloaded Workflow" ; + sh:name "Downloaded workflow distribution MUST match the download object" ; sh:description "" ; sh:severity sh:Violation ; @@ -130,7 +130,7 @@ five-safes-crate:DownloadedWorkflowDistributionAndDownloadActionObjectMUSTBeTheS five-safes-crate:DownloadActionActionStatusMUSTHaveAllowedValues a sh:NodeShape ; - sh:name "DownloadAction" ; + sh:name "DownloadAction MUST have an allowed action status" ; sh:description "" ; sh:target [ @@ -149,7 +149,7 @@ five-safes-crate:DownloadActionActionStatusMUSTHaveAllowedValues sh:property [ a sh:PropertyShape ; - sh:name "ActionStatus" ; + sh:name "DownloadAction action status" ; sh:path schema:actionStatus ; sh:in ( "http://schema.org/PotentialActionStatus" @@ -166,7 +166,7 @@ five-safes-crate:DownloadActionActionStatusMUSTHaveAllowedValues five-safes-crate:DownloadActionEntitySHOULDExist a sh:NodeShape ; - sh:name "RootDataEntity" ; + sh:name "DownloadAction SHOULD exist" ; sh:description "" ; sh:targetClass ro-crate:RootDataEntity ; sh:severity sh:Warning ; @@ -192,7 +192,7 @@ five-safes-crate:DownloadActionEntitySHOULDExist five-safes-crate:RootDataEntitySHOULDMentionDownloadActionIfPresent a sh:NodeShape ; - sh:name "RootDataEntity" ; + sh:name "Root data entity SHOULD mention DownloadAction when present" ; sh:description "" ; sh:targetClass ro-crate:RootDataEntity ; sh:severity sh:Warning ; @@ -218,7 +218,7 @@ five-safes-crate:RootDataEntitySHOULDMentionDownloadActionIfPresent five-safes-crate:DownloadActionShouldHaveEndTimeIfEnded a sh:NodeShape ; - sh:name "DownloadAction" ; + sh:name "DownloadAction SHOULD have an end time when ended" ; sh:description "" ; sh:target [ a sh:SPARQLTarget ; @@ -240,7 +240,7 @@ five-safes-crate:DownloadActionShouldHaveEndTimeIfEnded sh:property [ a sh:PropertyShape ; - sh:name "EndTime" ; + sh:name "DownloadAction end time" ; sh:path schema:endTime ; sh:minCount 1 ; sh:maxCount 1 ; @@ -251,13 +251,13 @@ five-safes-crate:DownloadActionShouldHaveEndTimeIfEnded five-safes-crate:DownloadActionShouldHaveActionStatus a sh:NodeShape ; - sh:name "DownloadAction" ; + sh:name "DownloadAction SHOULD have an action status" ; sh:targetClass schema:DownloadAction ; sh:description "" ; sh:property [ a sh:PropertyShape ; - sh:name "ActionStatus" ; + sh:name "DownloadAction action status" ; sh:path schema:actionStatus ; sh:minCount 1 ; sh:severity sh:Warning ; @@ -269,7 +269,7 @@ five-safes-crate:DownloadActionShouldHaveActionStatus five-safes-crate:DownloadedWorkflowSHOULDExistAndBeReferencedByDownloadActionResult a sh:NodeShape ; - sh:name "DownloadAction" ; + sh:name "DownloadAction result MAY reference an existing workflow entity" ; sh:description "Validates that DownloadAction result references an existing entity" ; sh:targetClass schema:DownloadAction ; @@ -304,7 +304,7 @@ five-safes-crate:DownloadedWorkflowSHOULDExistAndBeReferencedByDownloadActionRes five-safes-crate:DownloadActionMayHaveStartTimeIfBegun a sh:NodeShape ; - sh:name "DownloadAction" ; + sh:name "DownloadAction MAY have a start time when begun" ; sh:description ( "`DownloadAction` MAY have the `startTime` property if `actionStatus` " "is either ActiveActionStatus, CompletedActionStatus or FailedActionStatus." @@ -331,7 +331,7 @@ five-safes-crate:DownloadActionMayHaveStartTimeIfBegun sh:property [ a sh:PropertyShape ; - sh:name "StartTime" ; + sh:name "DownloadAction start time" ; sh:path schema:startTime ; sh:minCount 1 ; sh:maxCount 1 ; diff --git a/rocrate_validator/profiles/five-safes-crate/1_root_data_entity.ttl b/rocrate_validator/profiles/five-safes-crate/1_root_data_entity.ttl index b5481fd12..60c96854f 100644 --- a/rocrate_validator/profiles/five-safes-crate/1_root_data_entity.ttl +++ b/rocrate_validator/profiles/five-safes-crate/1_root_data_entity.ttl @@ -26,12 +26,12 @@ five-safes-crate:RootDataEntityRequiredProperties a sh:NodeShape ; - sh:name "RootDataEntity" ; + sh:name "Root data entity MUST identify its source organization" ; sh:targetClass ro-crate:RootDataEntity ; sh:property [ a sh:PropertyShape ; - sh:name "sourceOrganization" ; + sh:name "Root source organization presence" ; sh:path schema:sourceOrganization; sh:minCount 1 ; sh:severity sh:Violation ; @@ -40,7 +40,7 @@ five-safes-crate:RootDataEntityRequiredProperties sh:property [ a sh:PropertyShape ; - sh:name "sourceOrganization" ; + sh:name "Root source organization Project type" ; sh:path schema:sourceOrganization ; sh:class schema:Project ; sh:severity sh:Violation ; diff --git a/rocrate_validator/profiles/five-safes-crate/4_sign_off.ttl b/rocrate_validator/profiles/five-safes-crate/4_sign_off.ttl index 0c91901e4..770dc5576 100644 --- a/rocrate_validator/profiles/five-safes-crate/4_sign_off.ttl +++ b/rocrate_validator/profiles/five-safes-crate/4_sign_off.ttl @@ -78,7 +78,7 @@ five-safes-crate:SignOffObjectHasActionStatus sh:property [ a sh:PropertyShape ; - sh:name "actionStatus" ; + sh:name "SignOff action status" ; sh:description "The value of actionStatus MUST be one of the allowed values." ; sh:path schema:actionStatus ; sh:in ( @@ -248,7 +248,7 @@ five-safes-crate:SignOffPhaseEndTime sh:property [ a sh:PropertyShape ; - sh:name "EndTime" ; + sh:name "SignOff end time" ; sh:path schema:endTime ; sh:minCount 1 ; sh:maxCount 1 ; @@ -284,7 +284,7 @@ five-safes-crate:SignOffPhaseStartTime sh:property [ a sh:PropertyShape ; - sh:name "StartTime" ; + sh:name "SignOff start time" ; sh:path schema:startTime ; sh:minCount 1 ; sh:maxCount 1 ; diff --git a/rocrate_validator/profiles/five-safes-crate/6_workflow_reference.ttl b/rocrate_validator/profiles/five-safes-crate/6_workflow_reference.ttl index 222f2bc9b..4dc52c733 100644 --- a/rocrate_validator/profiles/five-safes-crate/6_workflow_reference.ttl +++ b/rocrate_validator/profiles/five-safes-crate/6_workflow_reference.ttl @@ -27,13 +27,13 @@ five-safes-crate:ReferenceToWorkflowCrate a sh:NodeShape ; - sh:name "RootDataEntity" ; + sh:name "Root data entity MUST reference a workflow crate" ; sh:targetClass ro-crate:RootDataEntity ; # RootDataEntity MUST have an mainEntity property sh:property [ a sh:PropertyShape ; - sh:name "mainEntity" ; + sh:name "Root mainEntity cardinality and IRI" ; sh:path schema:mainEntity ; sh:minCount 1 ; sh:maxCount 1 ; @@ -45,7 +45,7 @@ five-safes-crate:ReferenceToWorkflowCrate # The mainEntity of a RootDataEntity MUST be a Dataset sh:property [ a sh:PropertyShape ; - sh:name "mainEntity" ; + sh:name "Root mainEntity Dataset type" ; sh:path schema:mainEntity ; sh:class schema:Dataset ; sh:severity sh:Violation ; @@ -59,7 +59,7 @@ five-safes-crate:mainEntityHasProperConformsTo sh:targetObjectsOf schema:mainEntity ; sh:property [ a sh:PropertyShape ; - sh:name "conformsTo" ; + sh:name "Workflow conformsTo cardinality" ; sh:path purl:conformsTo ; sh:minCount 1 ; sh:maxCount 1 ; @@ -68,7 +68,7 @@ five-safes-crate:mainEntityHasProperConformsTo ] ; sh:property [ a sh:PropertyShape ; - sh:name "conformsTo" ; + sh:name "Workflow conformsTo profile IRI" ; sh:path purl:conformsTo ; sh:nodeKind sh:IRI ; sh:pattern "^https://w3id\\.org/workflowhub/workflow-ro-crate" ; diff --git a/rocrate_validator/profiles/five-safes-crate/7_requested_workflow_run.ttl b/rocrate_validator/profiles/five-safes-crate/7_requested_workflow_run.ttl index 9b66f1769..7cc1040ea 100644 --- a/rocrate_validator/profiles/five-safes-crate/7_requested_workflow_run.ttl +++ b/rocrate_validator/profiles/five-safes-crate/7_requested_workflow_run.ttl @@ -26,7 +26,7 @@ five-safes-crate:RootDataEntityMentionsWorkflowRunAction a sh:NodeShape ; - sh:name "RootDataEntity" ; + sh:name "Root data entity MUST mention a workflow run" ; sh:targetClass ro-crate:RootDataEntity ; sh:description "" ; @@ -94,7 +94,7 @@ five-safes-crate:WorkflowRunObject five-safes-crate:WorkflowRunActionShouldHaveObjectProperty a sh:NodeShape ; sh:targetClass ro-crate:WorkflowRunAction ; - sh:name "WorkflowRunAction" ; + sh:name "Workflow run SHOULD have an object" ; sh:property [ sh:path schema:object ; sh:minCount 1 ; diff --git a/rocrate_validator/profiles/five-safes-crate/8_disclosure_phase.ttl b/rocrate_validator/profiles/five-safes-crate/8_disclosure_phase.ttl index 61b785a43..436a26886 100644 --- a/rocrate_validator/profiles/five-safes-crate/8_disclosure_phase.ttl +++ b/rocrate_validator/profiles/five-safes-crate/8_disclosure_phase.ttl @@ -27,7 +27,7 @@ five-safes-crate:DisclosureObjectHasDescriptiveNameAndIsAssessAction a sh:NodeShape ; - sh:name "DisclosureCheck" ; + sh:name "DisclosureCheck MUST have a name and AssessAction type" ; sh:description "DisclosureCheck" ; sh:target [ @@ -56,7 +56,7 @@ five-safes-crate:DisclosureObjectHasDescriptiveNameAndIsAssessAction sh:property [ a sh:PropertyShape ; - sh:name "name" ; + sh:name "DisclosureCheck name" ; sh:description "`DisclosureCheck` MUST have a name string of at least 10 characters." ; sh:minCount 1 ; sh:path schema:name ; @@ -69,7 +69,7 @@ five-safes-crate:DisclosureObjectHasDescriptiveNameAndIsAssessAction five-safes-crate:DisclosureObjectHasActionStatusWithAcceptedValue a sh:NodeShape ; - sh:name "DisclosureCheck" ; + sh:name "DisclosureCheck MUST have an allowed action status" ; sh:description "`DisclosureCheck` MUST have an actionStatus with an allowed value (see https://schema.org/ActionStatusType)." ; sh:target [ @@ -88,7 +88,7 @@ five-safes-crate:DisclosureObjectHasActionStatusWithAcceptedValue sh:property [ a sh:PropertyShape ; - sh:name "actionStatus" ; + sh:name "DisclosureCheck action status" ; sh:description "`DisclosureCheck` MUST have an actionStatus with an allowed value (see https://schema.org/ActionStatusType)." ; sh:path schema:actionStatus ; sh:in ( @@ -106,7 +106,7 @@ five-safes-crate:DisclosureObjectHasActionStatusWithAcceptedValue five-safes-crate:RootDataEntityShouldMentionDisclosureObject a sh:NodeShape ; - sh:name "RootDataEntity" ; + sh:name "Root data entity SHOULD mention DisclosureCheck" ; sh:targetClass ro-crate:RootDataEntity ; sh:description "RootDataEntity SHOULD mention a disclosure object." ; sh:severity sh:Warning ; @@ -133,7 +133,7 @@ five-safes-crate:RootDataEntityShouldMentionDisclosureObject five-safes-crate:DisclosureObjectHasActionStatus a sh:NodeShape ; - sh:name "DisclosureCheck" ; + sh:name "DisclosureCheck SHOULD have an action status" ; sh:description "The `DisclosureCheck` SHOULD have `actionStatus` property." ; sh:target [ @@ -151,7 +151,7 @@ five-safes-crate:DisclosureObjectHasActionStatus sh:property [ a sh:PropertyShape ; - sh:name "ActionStatus" ; + sh:name "DisclosureCheck action status" ; sh:description "The `DisclosureCheck` SHOULD have `actionStatus` property." ; sh:path schema:actionStatus ; sh:minCount 1 ; @@ -162,7 +162,7 @@ five-safes-crate:DisclosureObjectHasActionStatus five-safes-crate:DisclosureObjectHasEndTimeIfcompletedOrFailed a sh:NodeShape ; - sh:name "DisclosureCheck" ; + sh:name "DisclosureCheck SHOULD have an end time when ended" ; sh:description "DisclosureCheck" ; sh:target [ @@ -185,7 +185,7 @@ five-safes-crate:DisclosureObjectHasEndTimeIfcompletedOrFailed sh:property [ a sh:PropertyShape ; - sh:name "EndTime" ; + sh:name "DisclosureCheck end time" ; sh:path schema:endTime ; sh:minCount 1 ; sh:maxCount 1 ; @@ -223,7 +223,7 @@ five-safes-crate:DisclosureObjectHasStartTimeIfBegun sh:property [ a sh:PropertyShape ; - sh:name "StartTime" ; + sh:name "DisclosureCheck start time" ; sh:path schema:startTime ; sh:minCount 1 ; sh:maxCount 1 ; diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py index bcc7a512f..b39a51207 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.1/must/0_file_descriptor_format.py @@ -20,46 +20,54 @@ from urllib.parse import urljoin from rocrate_validator.constants import HTTP_STATUS_OK -from rocrate_validator.models import ValidationContext +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging -from rocrate_validator.utils.http import HttpRequester +from rocrate_validator.utils.http import HttpRequester, OfflineCacheMissError +from rocrate_validator.utils.jsonld import find_unexpected_compaction_keys, resolve_compaction_context # set up logging logger = logging.getLogger(__name__) +_HANDLED_METADATA_ERRORS = (AssertionError, AttributeError, KeyError, TypeError, ValueError) +_HANDLED_REMOTE_CONTEXT_ERRORS = (*_HANDLED_METADATA_ERRORS, OSError, RuntimeError) + @requirement(name="File Descriptor existence") class FileDescriptorExistence(PyFunctionCheck): """The file descriptor MUST be present in the RO-Crate and MUST not be empty.""" @check(name="File Descriptor Existence") - def test_existence(self, context: ValidationContext) -> bool: + def test_existence(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor is present in the RO-Crate """ + # Dictionary input supplies the descriptor independently of payload storage. + if context.settings.metadata_dict is not None: + return True if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") - return True + context.record_skip(self, "metadata-only mode", "configured") + return CheckResult.SKIPPED if not context.ro_crate.has_descriptor(): message = f'file descriptor "{context.rel_fd_path}" is not present' context.result.add_issue(message, self) return False return True - @check(name="File Descriptor size check") - def test_size(self, context: ValidationContext) -> bool: + @check(name="File Descriptor size check", depends_on=("File Descriptor Existence",)) + def test_size(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor is not empty """ - if context.settings.metadata_only: + if context.settings.metadata_only and context.settings.metadata_dict is None: logger.debug("Skipping file descriptor existence check in metadata-only mode") - return True - if not context.ro_crate.has_descriptor(): - message = f"file descriptor {context.rel_fd_path} is empty" - context.result.add_issue(message, self) - return False - if context.ro_crate.metadata.size == 0: + context.record_skip(self, "metadata-only mode", "configured") + return CheckResult.SKIPPED + if ( + context.settings.metadata_dict is not None or context.ro_crate.has_descriptor() + ) and context.ro_crate.metadata.size == 0: context.result.add_issue(f'RO-Crate "{context.rel_fd_path}" file descriptor is empty', self) return False return True @@ -71,8 +79,8 @@ class FileDescriptorJsonFormat(PyFunctionCheck): The file descriptor MUST be a valid JSON file """ - @check(name="File Descriptor JSON format") - def check(self, context: ValidationContext) -> bool: + @check(name="File Descriptor JSON format", depends_on=("File Descriptor Existence",)) + def check(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor is in the correct format""" try: logger.debug("Checking validity of JSON file at %s", context.ro_crate.metadata) @@ -89,7 +97,14 @@ def check(self, context: ValidationContext) -> bool: # The metadata cannot be parsed: abort to avoid false positives downstream. context.abort_validation(f"file descriptor is not valid JSON: {e}") return False - except Exception: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor JSON check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _HANDLED_METADATA_ERRORS: context.result.add_issue( f'RO-Crate file descriptor "{context.rel_fd_path}" is not in the correct format', self ) @@ -187,7 +202,9 @@ def __check_remote_context__(self, context_uri: str) -> bool: a valid JSON-LD context: it is not a dictionary" ) return True - except Exception: + except OfflineCacheMissError: + raise + except _HANDLED_REMOTE_CONTEXT_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error validating JSON-LD context is a dictionary") return False @@ -212,9 +229,13 @@ def __check_contexts__(self, context: ValidationContext, jsonld_context: object) # return if the context is valid return is_valid - @check(name="File Descriptor @context property validation") - def check_context(self, context: ValidationContext) -> bool: - """Check if the file descriptor contains + @check( + name="File Descriptor @context property validation", + depends_on=("File Descriptor JSON format",), + ) + def check_context(self, context: ValidationContext) -> CheckResultValue: + """ + Check if the file descriptor contains the @context property and it is a valid JSON-LD context """ try: @@ -227,13 +248,23 @@ def check_context(self, context: ValidationContext) -> bool: # Check if the context is valid return self.__check_contexts__(context, json_dict["@context"]) - except Exception: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor JSON-LD context check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error extracting @context from file descriptor") return False - @check(name="File Descriptor JSON-LD must be flattened") - def check_flattened(self, context: ValidationContext) -> bool: + @check( + name="File Descriptor JSON-LD must be flattened", + depends_on=("File Descriptor JSON format",), + ) + def check_flattened(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor is flattened""" return self._check_flattened_graph( context, @@ -320,13 +351,23 @@ def _check_flattened_graph(self, context, is_flat): if fail_fast: return False return result - except Exception: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor flattening check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error flattening JSON-LD file descriptor") return False - @check(name="Validation of the @id property of the file descriptor entities") - def check_identifiers(self, context: ValidationContext) -> bool: + @check( + name="Validation of the @id property of the file descriptor entities", + depends_on=("File Descriptor JSON format",), + ) + def check_identifiers(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor entities have the @id property""" try: json_dict = context.ro_crate.metadata.as_dict() @@ -338,15 +379,26 @@ def check_identifiers(self, context: ValidationContext) -> bool: "file descriptor does not contain the @id attribute", self, ) + context.abort_validation("file descriptor entity does not contain the @id attribute") return False return True - except Exception: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error validating @id property of file descriptor entities") return False - @check(name="Validation of the @type property of the file descriptor entities") - def check_types(self, context: ValidationContext) -> bool: + @check( + name="Validation of the @type property of the file descriptor entities", + depends_on=("File Descriptor JSON format",), + ) + def check_types(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor entities have the @type property""" try: json_dict = context.ro_crate.metadata.as_dict() @@ -360,7 +412,14 @@ def check_types(self, context: ValidationContext) -> bool: ) return False return True - except Exception: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor type check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error validating @type property of file descriptor entities") return False @@ -394,51 +453,11 @@ def __get_remote_context_keys__(self, context_uri: str) -> set: raise TypeError("The context is not a dictionary") return set(jsonLD_ctx.keys()) - # Reserved JSON-LD keywords that are always allowed as entity keys. - __RESERVED_JSONLD_KEYS__ = frozenset({"@id", "@type", "@context", "@value", "@language"}) - - def __check_entity_keys__( - self, entity: Any, context_keys: set, unexpected_keys: dict[str, int] | None = None - ) -> dict[str, int]: - """Check if the entity is in the correct format""" - # Ensure unexpected_keys is initialized - if unexpected_keys is None: - unexpected_keys = {} - - # If the entity is a dictionary, classify each key and recurse into values - if isinstance(entity, dict): - for k, v in entity.items(): - self.__record_unexpected_key__(k, context_keys, unexpected_keys) - # If the value is a dictionary or a list, check its keys recursively - if isinstance(v, (dict, list)): - self.__check_entity_keys__(v, context_keys, unexpected_keys) - - # If the entity is a list, check each element - elif isinstance(entity, list): - for elem in entity: - self.__check_entity_keys__(elem, context_keys, unexpected_keys) - - return unexpected_keys - - def __record_unexpected_key__(self, k: str, context_keys: set, unexpected_keys: dict[str, int]) -> None: - """Record ``k`` as unexpected unless it is reserved or a valid compact IRI prefix""" - # If the key is a reserved JSON-LD keyword, skip it - if k in self.__RESERVED_JSONLD_KEYS__: - logger.debug(f"Key {k} is a reserved JSON-LD keyword, skipping") - return - - # A key not in the context can still be valid in compacted format if it is - # a compact IRI whose prefix is in the context. - if k not in context_keys: - logger.debug(f"Key {k} not in context keys") - prefix = k.split(":", 1)[0] if ":" in k else None - logger.debug(f"Checking prefix {prefix} of key {k}") - if prefix is None or prefix not in context_keys: - logger.debug(f"Key {k} does not have a valid prefix in context keys, adding to unexpected keys") - unexpected_keys[k] = unexpected_keys.get(k, 0) + 1 - - @check(name="Validation of the compaction format of the file descriptor") - def check_compaction(self, context: ValidationContext) -> bool: + @check( + name="Validation of the compaction format of the file descriptor", + depends_on=("File Descriptor JSON format",), + ) + def check_compaction(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 """Check if the file descriptor is in the **compacted** JSON-LD format""" try: logger.debug("Checking compaction format of JSON-LD file at %s", context.ro_crate.metadata) @@ -449,15 +468,17 @@ def check_compaction(self, context: ValidationContext) -> bool: logger.debug(f"Context: {jsonld_context}") try: - context_keys = self.__get_context_keys__(jsonld_context) - logger.debug(f"{context_keys}") - except Exception as e: + context_definitions = resolve_compaction_context(jsonld_context, self.__get_remote_context__) + logger.debug(f"{context_definitions}") + except OfflineCacheMissError: + raise + except _HANDLED_REMOTE_CONTEXT_ERRORS as e: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error getting context keys from JSON-LD") context.result.add_issue(str(e), self) return False - unexpected_keys = self.__check_entity_keys__(json_dict.get("@graph"), context_keys) + unexpected_keys = find_unexpected_compaction_keys(json_dict.get("@graph"), context_definitions) logger.debug(f"Unexpected keys: {unexpected_keys}") if len(unexpected_keys) > 0: for k, v in unexpected_keys.items(): @@ -467,8 +488,8 @@ def check_compaction(self, context: ValidationContext) -> bool: # Check if k is a term or a URI if k.startswith("http"): context.result.add_issue( - f'The {v} occurrence{suffix} of the "{k}" URI cannot be used as a key{suffix} "' - "because the compacted format requires simple terms as keys " + f'The {v} occurrence{suffix} of the "{k}" URI cannot be used as a key{suffix} ' + "because it is not mapped by the document context to a term or compact IRI " "(see https://www.w3.org/TR/json-ld-api/#compaction for more details).", self, ) @@ -476,13 +497,20 @@ def check_compaction(self, context: ValidationContext) -> bool: context.result.add_issue( f'The {v} occurrence{suffix} of the JSON-LD key "{k}" ' f"{'is' if v == 1 else 'are'} not allowed in the compacted format " - "because it is not present in the @context of the document", + "because it is not defined as a term or compact IRI by the @context of the document", self, ) return False return True - except Exception as e: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor compaction check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _HANDLED_METADATA_ERRORS as e: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") context.result.add_issue(f"Unexpected error: {e}", self) diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.1/must/1_file-descriptor_metadata.ttl index 964569bbd..0258739ef 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/1_file-descriptor_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.1/must/1_file-descriptor_metadata.ttl @@ -37,7 +37,10 @@ ro-crate:ROCrateMetadataFileDescriptorExistence a sh:NodeShape ; ?crate a schema:Dataset . ?md schema:about ?crate . ?md a schema:CreativeWork . - FILTER( STRENDS(STR(?md), "ro-crate-metadata.json") ) + FILTER( + STRENDS(STR(?md), "ro-crate-metadata.json") || + STRENDS(STR(?md), "ro-crate-metadata.jsonld") + ) } } """ ; @@ -56,7 +59,10 @@ ro-crate:FindROCrateMetadataFileDescriptorEntity a sh:NodeShape, validator:Hidde WHERE { ?crate a schema:Dataset . ?this schema:about ?crate . - FILTER(contains(str(?this), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?this), "ro-crate-metadata.json") || + STRENDS(str(?this), "ro-crate-metadata.jsonld") + ) } """ ] ; diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/2_root_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.1/must/2_root_data_entity_metadata.ttl index b502d29c0..45c586784 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/2_root_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.1/must/2_root_data_entity_metadata.ttl @@ -32,7 +32,10 @@ ro-crate:RootDataEntityType SELECT ?this WHERE { ?metadatafile schema:about ?this . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) } """ ] ; @@ -74,7 +77,10 @@ ro-crate:FindRootDataEntity a sh:NodeShape, validator:HiddenShape; WHERE { ?this a schema:Dataset . ?metadatafile schema:about ?this . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) } """ ] ; diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.py index 7aea2a2c6..4a943a7f9 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.py @@ -14,7 +14,8 @@ # pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) -from rocrate_validator.models import ValidationContext +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -29,20 +30,27 @@ class DataEntityRequiredChecker(PyFunctionCheck): """ @check(name="Data Entity: REQUIRED resource availability") - def check_availability(self, context: ValidationContext) -> bool: + def check_availability(self, context: ValidationContext) -> CheckResultValue: """ Check the presence of the Data Entity in the RO-Crate """ # Skip the check in metadata-only mode if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") - return True + context.record_skip(self, "metadata-only mode", "configured") + return CheckResult.SKIPPED # Perform the check result = True # Web-based Data Entities (absolute URIs with any scheme other than `file`, # e.g. http://, https://, ftp://, scp://, s3://, ...) are not required to # be part of the local payload per the RO-Crate specification. - for entity in context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True): + try: + entities = context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + for entity in entities: assert entity.id is not None, "Entity has no @id" logger.debug("Ensure the presence of the Data Entity '%s' within the RO-Crate", entity.id) try: @@ -70,7 +78,7 @@ def check_availability(self, context: ValidationContext) -> bool: f"The RO-Crate does not include the Data Entity '{entity.id}' as part of its payload", self ) result = False - except Exception as e: + except (AttributeError, OSError, TypeError, ValueError) as e: context.result.add_issue( f"Unable to check the the presence of the Data Entity '{entity.id}' within the RO-Crate", self ) diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.ttl index 2a458045d..6964e570f 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.1/must/4_data_entity_metadata.ttl @@ -21,7 +21,7 @@ @prefix validator: . ro-crate:DataEntityRequiredProperties a sh:NodeShape ; - sh:name "Data Entity: REQUIRED properties" ; + sh:name "Data Entity: REQUIRED @id format" ; sh:description """A Data Entity MUST be a `URI Path` relative to the ROCrate root, or an absolute URI""" ; sh:targetClass ro-crate:DataEntity ; @@ -47,9 +47,14 @@ ro-crate:FileDataEntity a sh:NodeShape ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) + } } ?this a schema:MediaObject . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) } """ @@ -86,10 +91,15 @@ ro-crate:DirectoryDataEntity a sh:NodeShape ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) + } } ?this a schema:Dataset . - ?metadatafile schema:about ?root . # Exclude all dataset entities that ends with `./#` - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER(?this != ?root) FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) } @@ -131,8 +141,8 @@ ro-crate:DirectoryDataEntity a sh:NodeShape ; ] . ro-crate:DataEntityRequiredPropertiesShape a sh:NodeShape ; - sh:name "Data Entity: REQUIRED properties" ; - sh:description """A `DataEntity`The file descriptor MUST be a valid JSON-LD file MUST be linked, either directly or indirectly, from the Root Data Entity""" ; + sh:name "Data Entity: REQUIRED Root linkage" ; + sh:description """A `DataEntity` MUST be linked, either directly or indirectly, from the Root Data Entity""" ; sh:targetClass ro-crate:DataEntity ; sh:property [ @@ -157,9 +167,14 @@ ro-crate:GenericDataEntityRequiredProperties a sh:NodeShape ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root ?metadatafile WHERE { + ?metadatafile schema:about ?root . + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) + } } ?root schema:hasPart ?this . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER(?this != ?root) FILTER(?this != ?metadatafile) FILTER NOT EXISTS { diff --git a/rocrate_validator/profiles/ro-crate/1.1/must/6_contextual_entity.ttl b/rocrate_validator/profiles/ro-crate/1.1/must/6_contextual_entity.ttl index 9ecf87a9d..7191408a0 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/must/6_contextual_entity.ttl +++ b/rocrate_validator/profiles/ro-crate/1.1/must/6_contextual_entity.ttl @@ -25,16 +25,7 @@ ro-crate:FindLicenseEntity a sh:NodeShape, validator:HiddenShape ; sh:name "Identify License Entity" ; sh:description """Mark a license entity any Data Entity referenced by the `schema:license` property.""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?subject schema:license ?this . - } - """ - ] ; + sh:targetObjectsOf schema:license ; # Expand data graph with triples from the file data entity sh:rule [ diff --git a/rocrate_validator/profiles/ro-crate/1.1/should/2_root_data_entity_relative_uri.py b/rocrate_validator/profiles/ro-crate/1.1/should/2_root_data_entity_relative_uri.py index a67458c34..fde5a7fb3 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/should/2_root_data_entity_relative_uri.py +++ b/rocrate_validator/profiles/ro-crate/1.1/should/2_root_data_entity_relative_uri.py @@ -14,7 +14,8 @@ # pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) -from rocrate_validator.models import ValidationContext +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -29,13 +30,17 @@ class RootDataEntityRelativeURI(PyFunctionCheck): """ @check(name="Root Data Entity: RECOMMENDED value") - def check_relative_uris(self, context: ValidationContext) -> bool: + def check_relative_uris(self, context: ValidationContext) -> CheckResultValue: """Check if the Root Data Entity is denoted by the string `./` in the file descriptor JSON-LD""" try: if context.ro_crate.metadata.get_root_data_entity().id != "./": context.result.add_issue("Root Data Entity URI is not denoted by the string `./`", self) return False return True - except Exception as e: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Root Data Entity URI check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except (AttributeError, TypeError, ValueError) as e: context.result.add_issue(f"Error checking Root Data Entity URI: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.1/should/3_entity_property_compact_representation_1_1.py b/rocrate_validator/profiles/ro-crate/1.1/should/3_entity_property_compact_representation_1_1.py new file mode 100644 index 000000000..b0a92533f --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.1/should/3_entity_property_compact_representation_1_1.py @@ -0,0 +1,59 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) + +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging +from rocrate_validator.utils.jsonld import find_singleton_property_arrays + +logger = logging.getLogger(__name__) + + +@requirement(name="Entity properties compact representation") +class EntityPropertyCompactRepresentation(PyFunctionCheck): + """ + Single-element arrays used for entity properties SHOULD be unpacked to a + single value in compacted RO-Crate 1.1 JSON-LD. + """ + + @check(name="Entity properties SHOULD use single values rather than singleton arrays") + def check_singleton_property_arrays(self, context: ValidationContext) -> bool: + """Check the RO-Crate 1.1 recommendation for singleton property arrays.""" + try: + findings = find_singleton_property_arrays(context.ro_crate.metadata.as_dict()) + except ROCrateMetadataNotFoundError as error: + logger.debug("Skipping singleton property array check: metadata descriptor is not available (%s)", error) + return True + except (AttributeError, TypeError, ValueError) as error: + # Required format checks report malformed metadata structure. + logger.debug("Skipping singleton property array check: metadata cannot be inspected (%s)", error) + return True + + result = True + for entity_id, property_name in findings: + context.result.add_issue( + f'Entity "{entity_id}" property "{property_name}" SHOULD be represented as a single value, ' + "not a singleton array, according to the RO-Crate 1.1 JSON-LD recommendation", + self, + violatingEntity=entity_id, + violatingProperty=property_name, + violatingPropertyValue="singleton array", + ) + result = False + if context.fail_fast: + return result + return result diff --git a/rocrate_validator/profiles/ro-crate/1.1/should/4_data_entity_existence.py b/rocrate_validator/profiles/ro-crate/1.1/should/4_data_entity_existence.py index c770521c3..07df96062 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/should/4_data_entity_existence.py +++ b/rocrate_validator/profiles/ro-crate/1.1/should/4_data_entity_existence.py @@ -14,7 +14,8 @@ # pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) -from rocrate_validator.models import ValidationContext +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -30,7 +31,7 @@ class DataEntityRecommendedChecker(PyFunctionCheck): """ @check(name="Data Entity: RECOMMENDED resource availability") - def check_availability(self, context: ValidationContext) -> bool: + def check_availability(self, context: ValidationContext) -> CheckResultValue: """ Check the availability of the Data Entity with absolute URI paths are available at the time of RO-Crate creation @@ -38,20 +39,27 @@ def check_availability(self, context: ValidationContext) -> bool: # Skip the check in metadata-only mode if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") - return True + context.record_skip(self, "metadata-only mode", "configured") + return CheckResult.SKIPPED # Perform the check result = True - for entity in [ - _ - for _ in context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) - if _.has_absolute_path() - ]: + try: + entities = [ + _ + for _ in context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) + if _.has_absolute_path() + ] + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + for entity in entities: assert entity.id is not None, "Entity has no @id" try: if not entity.is_available(): context.result.add_issue(f"Data Entity {entity.id} is not available", self) result = False - except Exception as e: + except (AttributeError, OSError, TypeError, ValueError) as e: context.result.add_issue(f"Web-based Data Entity {entity.id} is not available: {e}", self) result = False if not result and context.fail_fast: diff --git a/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py index eb821df23..5f1bdb98a 100644 --- a/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.1/should/5_web_data_entity_metadata.py @@ -14,7 +14,8 @@ # pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) -from rocrate_validator.models import ValidationContext +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.uri import AvailabilityStatus @@ -31,7 +32,7 @@ class WebDataEntityRecommendedChecker(PyFunctionCheck): """ @check(name="Web-based Data Entity: resource availability") - def check_availability(self, context: ValidationContext) -> bool: + def check_availability(self, context: ValidationContext) -> CheckResultValue: """ Check if the Web-based Data Entity is directly downloadable by a simple retrieval (e.g. HTTP GET) permitting redirection and HTTP/HTTPS URIs. @@ -42,7 +43,13 @@ def check_availability(self, context: ValidationContext) -> bool: and logged as warnings, without invalidating the validation. """ result = True - for entity in context.ro_crate.metadata.get_web_data_entities(): + try: + entities = context.ro_crate.metadata.get_web_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping web-based Data Entity availability check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + for entity in entities: assert entity.id is not None, "Entity has no @id" try: status = entity.check_availability() @@ -67,21 +74,27 @@ def check_availability(self, context: ValidationContext) -> bool: else: context.result.add_issue(f"Web-based Data Entity {entity.id} is not available", self) result = False - except Exception as e: + except (OSError, RuntimeError, TypeError, ValueError) as e: context.result.add_issue(f"Web-based Data Entity {entity.id} is not available: {e}", self) result = False if not result and context.fail_fast: return result return result - @check(name="Web-based Data Entity: `contentSize` property") - def check_content_size(self, context: ValidationContext) -> bool: + @check(name="Web-based Data Entity: `contentSize` matches downloadable content") + def check_content_size(self, context: ValidationContext) -> CheckResultValue: """ Check if the Web-based Data Entity has a `contentSize` property and if it is set to actual size of the downloadable content """ result = True - for entity in context.ro_crate.metadata.get_web_data_entities(): + try: + entities = context.ro_crate.metadata.get_web_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping web-based Data Entity content size check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + for entity in entities: assert entity.id is not None, "Entity has no @id" # Skip entities whose scheme the validator cannot natively fetch # (e.g. scp://, s3://): without retrieving the content there is @@ -91,7 +104,7 @@ def check_content_size(self, context: ValidationContext) -> bool: continue if entity.is_available(): content_size = entity.get_property("contentSize") - actual_size = context.ro_crate.get_external_file_size(entity.id) + actual_size = context.ro_crate.get_external_file_size(entity.id_as_uri.uri) if content_size and actual_size is not None and int(content_size) != actual_size: context.result.add_issue( f"The property contentSize={content_size} of the Web-based Data Entity " diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py index 5c230f089..51506351c 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/0_file_descriptor_format.py @@ -19,14 +19,19 @@ from typing import Any from urllib.parse import urljoin -from rocrate_validator.models import ValidationContext +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging -from rocrate_validator.utils.http import HttpRequester +from rocrate_validator.utils.http import HttpRequester, OfflineCacheMissError +from rocrate_validator.utils.jsonld import find_unexpected_compaction_keys, resolve_compaction_context # set up logging logger = logging.getLogger(__name__) +_HANDLED_METADATA_ERRORS = (AssertionError, AttributeError, KeyError, TypeError, ValueError) +_HANDLED_REMOTE_CONTEXT_ERRORS = (*_HANDLED_METADATA_ERRORS, OSError, RuntimeError) + # Property keys whose string values are literals (e.g. names, dates, URLs) rather # than references to other entities, so a matching @id must not be flagged. _LITERAL_KEYS = frozenset( @@ -59,32 +64,41 @@ class FileDescriptorExistence(PyFunctionCheck): """The file descriptor MUST be present in the RO-Crate and MUST not be empty.""" @check(name="File Descriptor Existence") - def test_existence(self, context: ValidationContext) -> bool: + def test_existence(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor is present in the RO-Crate """ + # Dictionary input supplies the descriptor independently of payload storage. + if context.settings.metadata_dict is not None: + return True if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") - return True - if not context.ro_crate.has_descriptor(): - message = f'file descriptor "{context.rel_fd_path}" is not present' + context.record_skip(self, "metadata-only mode", "configured") + return CheckResult.SKIPPED + if context.ro_crate.is_attached(): + descriptor = context.ro_crate.attached_descriptor_id + exists = context.ro_crate.has_file(Path(descriptor)) + else: + descriptor = str(context.rel_fd_path) + exists = context.ro_crate.has_descriptor() + if not exists: + message = f'file descriptor "{descriptor}" is not present' context.result.add_issue(message, self) return False return True - @check(name="File Descriptor size check") - def test_size(self, context: ValidationContext) -> bool: + @check(name="File Descriptor size check", depends_on=("File Descriptor Existence",)) + def test_size(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor is not empty """ - if context.settings.metadata_only: + if context.settings.metadata_only and context.settings.metadata_dict is None: logger.debug("Skipping file descriptor existence check in metadata-only mode") - return True - if not context.ro_crate.has_descriptor(): - message = f"file descriptor {context.rel_fd_path} is empty" - context.result.add_issue(message, self) - return False - if context.ro_crate.metadata.size == 0: + context.record_skip(self, "metadata-only mode", "configured") + return CheckResult.SKIPPED + if ( + context.settings.metadata_dict is not None or context.ro_crate.has_descriptor() + ) and context.ro_crate.metadata.size == 0: context.result.add_issue(f'RO-Crate "{context.rel_fd_path}" file descriptor is empty', self) return False return True @@ -96,24 +110,31 @@ class FileDescriptorEncodingCheck(PyFunctionCheck): The file descriptor MUST be UTF-8 encoded """ - @check(name="File Descriptor UTF-8 encoding") - def check(self, context: ValidationContext) -> bool: + @check(name="File Descriptor UTF-8 encoding", depends_on=("File Descriptor Existence",)) + def check(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor is UTF-8 encoded """ + if context.settings.metadata_only and context.settings.metadata_dict is None: + logger.debug("Skipping file descriptor encoding check in metadata-only mode") + context.record_skip(self, "metadata-only mode", "configured") + return CheckResult.SKIPPED try: - raw_data = context.ro_crate.get_file_content( - Path(context.ro_crate.metadata_descriptor_id), binary_mode=True - ) + raw_data = context.ro_crate.metadata.as_bytes() if isinstance(raw_data, str): return True raw_data.decode("utf-8") return True - except Exception: + except UnicodeDecodeError: context.result.add_issue(f'RO-Crate file descriptor "{context.rel_fd_path}" is not UTF-8 encoded', self) + context.abort_validation("file descriptor is not UTF-8 encoded") if logger.isEnabledFor(logging.DEBUG): - logger.exception("Unexpected error during file descriptor validation") + logger.debug("RO-Crate file descriptor is not UTF-8 encoded", exc_info=True) return False + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor encoding check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED @requirement(name="File Descriptor JSON format") @@ -122,8 +143,8 @@ class FileDescriptorJsonFormat(PyFunctionCheck): The file descriptor MUST be a valid JSON file """ - @check(name="File Descriptor JSON format") - def check(self, context: ValidationContext) -> bool: + @check(name="File Descriptor JSON format", depends_on=("File Descriptor UTF-8 encoding",)) + def check(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor is in the correct format""" try: logger.debug("Checking validity of JSON file at %s", context.ro_crate.metadata) @@ -140,7 +161,14 @@ def check(self, context: ValidationContext) -> bool: # The metadata cannot be parsed: abort to avoid false positives downstream. context.abort_validation(f"file descriptor is not valid JSON: {e}") return False - except Exception: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor JSON check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _HANDLED_METADATA_ERRORS: context.result.add_issue( f'RO-Crate file descriptor "{context.rel_fd_path}" is not in the correct format', self ) @@ -223,7 +251,9 @@ def __check_remote_context__(self, context_uri: str) -> bool: jsonLD = self.__get_remote_context__(context_uri) assert isinstance(jsonLD, dict) return True - except Exception: + except OfflineCacheMissError: + raise + except _HANDLED_REMOTE_CONTEXT_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -248,9 +278,13 @@ def __check_contexts__(self, context: ValidationContext, jsonld_context: object) # return if the context is valid return is_valid - @check(name="File Descriptor @context property validation") - def check_context(self, context: ValidationContext) -> bool: - """Check if the file descriptor contains + @check( + name="File Descriptor @context property validation", + depends_on=("File Descriptor JSON format",), + ) + def check_context(self, context: ValidationContext) -> CheckResultValue: + """ + Check if the file descriptor contains the @context property and it is a valid JSON-LD context """ try: @@ -281,7 +315,14 @@ def has_expected_context(ctx: object) -> bool: # Check if the context is valid return self.__check_contexts__(context, json_dict["@context"]) - except Exception: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor JSON-LD context check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -362,8 +403,11 @@ def _is_entity_flat(self, context: ValidationContext, entity: Any, is_first: boo return False return result - @check(name="File Descriptor JSON-LD must be flattened") - def check_flattened(self, context: ValidationContext) -> bool: + @check( + name="File Descriptor JSON-LD must be flattened", + depends_on=("File Descriptor JSON format",), + ) + def check_flattened(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor is flattened""" try: fail_fast = bool(context.settings.abort_on_first) @@ -380,13 +424,23 @@ def check_flattened(self, context: ValidationContext) -> bool: if fail_fast: return False return result - except Exception: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor flattening check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False - @check(name="Validation of the @id property of the file descriptor entities") - def check_identifiers(self, context: ValidationContext) -> bool: + @check( + name="Validation of the @id property of the file descriptor entities", + depends_on=("File Descriptor JSON format",), + ) + def check_identifiers(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor entities have the @id property""" try: json_dict = context.ro_crate.metadata.as_dict() @@ -398,15 +452,26 @@ def check_identifiers(self, context: ValidationContext) -> bool: "file descriptor does not contain the @id attribute", self, ) + context.abort_validation("file descriptor entity does not contain the @id attribute") return False return True - except Exception: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False - @check(name="Validation of the @type property of the file descriptor entities") - def check_types(self, context: ValidationContext) -> bool: + @check( + name="Validation of the @type property of the file descriptor entities", + depends_on=("File Descriptor JSON format",), + ) + def check_types(self, context: ValidationContext) -> CheckResultValue: """Check if the file descriptor entities have the @type property""" try: json_dict = context.ro_crate.metadata.as_dict() @@ -420,13 +485,23 @@ def check_types(self, context: ValidationContext) -> bool: ) return False return True - except Exception: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor type check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False - @check(name="Validation of unique @id values") - def check_unique_identifiers(self, context: ValidationContext) -> bool: + @check( + name="Validation of unique @id values", + depends_on=("File Descriptor JSON format",), + ) + def check_unique_identifiers(self, context: ValidationContext) -> CheckResultValue: try: json_dict = context.ro_crate.metadata.as_dict() identifiers = [entity.get("@id") for entity in json_dict.get("@graph", [])] @@ -437,7 +512,14 @@ def check_unique_identifiers(self, context: ValidationContext) -> bool: ) return False return True - except Exception: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping unique identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -450,7 +532,10 @@ def _find_reference_issue(self, value: Any, entity_id: str, identifiers: set, ke """ if isinstance(value, str): if key not in _LITERAL_KEYS and value in identifiers: - return f"Entity '{entity_id}' references '{value}' as a string; use {{\"@id\": \"{value}\"}}" + return ( + "The string value matches a declared entity @id; verify whether this property " + "should use an @id reference" + ) return None if isinstance(value, list): children: Any = value @@ -466,8 +551,12 @@ def _find_reference_issue(self, value: Any, entity_id: str, identifiers: set, ke return message return None - @check(name="Validation of entity references") - def check_entity_references(self, context: ValidationContext) -> bool: + @check( + name="Heuristic extra_checks for suspicious entity references", + depends_on=("File Descriptor JSON format",), + extra_check=True, + ) + def check_entity_references(self, context: ValidationContext) -> CheckResultValue: try: json_dict = context.ro_crate.metadata.as_dict() graph = json_dict.get("@graph", []) @@ -480,16 +569,32 @@ def check_entity_references(self, context: ValidationContext) -> bool: continue message = self._find_reference_issue(value, entity_id, identifiers, key) if message: - context.result.add_issue(message, self) - return False + context.result.add_extra_check( + self, + message, + violating_entity=entity_id, + violating_property=key, + value=value if isinstance(value, str) else None, + ) return True - except Exception: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping entity reference check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _HANDLED_METADATA_ERRORS as error: if logger.isEnabledFor(logging.DEBUG): - logger.exception("Unexpected error during file descriptor validation") - return False - - @check(name="Validation of subject and keyword properties") - def check_subject_keywords(self, context: ValidationContext) -> bool: + logger.exception("Unable to inspect metadata for heuristic extra_checks") + context.record_skip(self, f"unable to inspect metadata for heuristic extra_checks: {error}", "exception") + return CheckResult.SKIPPED + + @check( + name="Validation of subject and keyword properties", + depends_on=("File Descriptor JSON format",), + ) + def check_subject_keywords(self, context: ValidationContext) -> CheckResultValue: try: json_dict = context.ro_crate.metadata.as_dict() graph = json_dict.get("@graph", []) @@ -502,7 +607,14 @@ def check_subject_keywords(self, context: ValidationContext) -> bool: context.result.add_issue(f"Entity '{entity_id}' should use schema.org 'keywords'", self) return False return True - except Exception: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping subject and keyword check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _HANDLED_METADATA_ERRORS: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") return False @@ -542,40 +654,11 @@ def __get_remote_context_keys__(self, context_uri: str) -> set: raise TypeError("The context is not a dictionary") return set(jsonLD_ctx.keys()) - def __check_entity_keys__( - self, entity: object, context_keys: set, unexpected_keys: dict[str, int] | None = None - ) -> dict[str, int]: - """Check if the entity is in the correct format""" - - def add_unexpected_key(k: str, u_keys: dict) -> None: - """Add a key to the unexpected keys dictionary""" - u_keys[k] = u_keys.get(k, 0) + 1 - - # Keys that should be skipped - SKIP_KEYS = {"@id", "@type", "@context", "@value", "@language"} - - # Ensure unexpected_keys is initialized - if unexpected_keys is None: - unexpected_keys = {} - - # If the entity is a dictionary, check each key - if isinstance(entity, dict): - for k, v in entity.items(): - if k not in context_keys and k not in SKIP_KEYS: - logger.debug(f"Key {k} not in context keys") - add_unexpected_key(k, unexpected_keys) - if isinstance(v, (dict, list)): - self.__check_entity_keys__(v, context_keys, unexpected_keys) - - # If the entity is a list, check each element - elif isinstance(entity, list): - for elem in entity: - self.__check_entity_keys__(elem, context_keys, unexpected_keys) - - return unexpected_keys - - @check(name="Validation of the compaction format of the file descriptor") - def check_compaction(self, context: ValidationContext) -> bool: + @check( + name="Validation of the compaction format of the file descriptor", + depends_on=("File Descriptor JSON format",), + ) + def check_compaction(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 """Check if the file descriptor is in the **compacted** JSON-LD format""" try: logger.debug("Checking compaction format of JSON-LD file at %s", context.ro_crate.metadata) @@ -586,15 +669,17 @@ def check_compaction(self, context: ValidationContext) -> bool: logger.debug(f"Context: {jsonld_context}") try: - context_keys = self.__get_context_keys__(jsonld_context) - logger.debug(f"{context_keys}") - except Exception as e: + context_definitions = resolve_compaction_context(jsonld_context, self.__get_remote_context__) + logger.debug(f"{context_definitions}") + except OfflineCacheMissError: + raise + except _HANDLED_REMOTE_CONTEXT_ERRORS as e: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") context.result.add_issue(str(e), self) return False - unexpected_keys = self.__check_entity_keys__(json_dict.get("@graph", []), context_keys) + unexpected_keys = find_unexpected_compaction_keys(json_dict.get("@graph", []), context_definitions) logger.debug(f"Unexpected keys: {unexpected_keys}") if len(unexpected_keys) > 0: for k, v in unexpected_keys.items(): @@ -604,8 +689,8 @@ def check_compaction(self, context: ValidationContext) -> bool: # Check if k is a term or a URI if k.startswith("http"): context.result.add_issue( - f'The The {v} occurrence{suffix} of the "{k}" URI cannot be used as a key{suffix} "' - "because the compacted format requires simple terms as keys " + f'The {v} occurrence{suffix} of the "{k}" URI cannot be used as a key{suffix} ' + "because it is not mapped by the document context to a term or compact IRI " "(see https://www.w3.org/TR/json-ld-api/#compaction for more details).", self, ) @@ -613,13 +698,20 @@ def check_compaction(self, context: ValidationContext) -> bool: context.result.add_issue( f'The {v} occurrence{suffix} of the JSON-LD key "{k}" ' f"{'is' if v == 1 else 'are'} not allowed in the compacted format " - "because it is not present in the @context of the document", + "because it is not defined as a term or compact IRI by the @context of the document", self, ) return False return True - except Exception as e: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping file descriptor compaction check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _HANDLED_METADATA_ERRORS as e: if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error during file descriptor validation") context.result.add_issue(f"Unexpected error: {e}", self) diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/1_file-descriptor_metadata.ttl index 6c90fb03a..6ce09ba1c 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/1_file-descriptor_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/1_file-descriptor_metadata.ttl @@ -32,7 +32,10 @@ ro-crate:FindROCrateMetadataFileDescriptorEntity a sh:NodeShape ; WHERE { ?crate a schema:Dataset . ?this schema:about ?crate . - FILTER(contains(str(?this), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?this), "ro-crate-metadata.json") || + STRENDS(str(?this), "ro-crate-metadata.jsonld") + ) } """ ] ; @@ -85,4 +88,12 @@ ro-crate:ROCrateMetadataFileDescriptorRecommendedProperties a sh:NodeShape ; sh:path schema_org:about ; # sh:class ro-crate:RootDataEntity ; sh:message "The RO-Crate metadata file descriptor MUST have an `about` property referencing the Root Data Entity" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Metadata File Descriptor `conformsTo` reference" ; + sh:description "If present, each `conformsTo` value MUST be expressed as a JSON-LD reference object with an `@id`." ; + sh:path dct:conformsTo ; + sh:nodeKind sh:IRI ; + sh:message "Each RO-Crate Metadata Descriptor `conformsTo` value MUST be a reference object containing `@id`; use {\"@id\": \"...\"} instead of a string." ; ] . diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py b/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py index ed1b2e281..d92b998b8 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/1_ro_crate_preview.py @@ -14,7 +14,8 @@ from pathlib import Path -from rocrate_validator.models import ValidationContext +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -29,18 +30,26 @@ class ROCrateWebsiteChecker(PyFunctionCheck): """ @check(name="RO-Crate Website HTML5 doctype") - def check_preview_html(self, context: ValidationContext) -> bool: - if context.ro_crate.is_detached(): - return True + def check_preview_html(self, context: ValidationContext) -> CheckResultValue: + try: + if not context.ro_crate.is_attached(): + context.record_skip(self, "attached package context is unavailable", "returned") + return CheckResult.SKIPPED + except ROCrateMetadataNotFoundError: + logger.debug("Skipping RO-Crate Website check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED preview_path = Path("ro-crate-preview.html") if not context.ro_crate.has_file(preview_path): return True try: content = context.ro_crate.get_file_content(preview_path, binary_mode=False) + if isinstance(content, bytes): + content = content.decode("utf-8", errors="strict") if " str | None: # cite-as can be a plain string literal or an entity reference {"@id": "..."} if isinstance(cite_as_raw, str): cite_as_url = cite_as_raw - elif hasattr(cite_as_raw, "id"): - cite_as_url = cite_as_raw.id else: - return None + entity_id = getattr(cite_as_raw, "id", None) + if not isinstance(entity_id, str): + return None + cite_as_url = entity_id if not cite_as_url or not cite_as_url.startswith("http"): return None return cite_as_url @check(name="Root Data Entity: `cite-as` MUST reference a downloadable item") - def check_cite_as_downloadable(self, context: ValidationContext) -> bool: + def check_cite_as_downloadable(self, context: ValidationContext) -> CheckResultValue: if ( context.settings.skip_availability_check or not (context.settings.creation_time or context.settings.enforce_availability) or context.settings.metadata_only ): - return True + context.record_skip(self, "availability check is disabled or not applicable", "configured") + return CheckResult.SKIPPED try: root_entity = context.ro_crate.metadata.get_root_data_entity() @@ -81,6 +84,10 @@ def check_cite_as_downloadable(self, context: ValidationContext) -> bool: ) return True - except Exception as e: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Root Data Entity cite-as check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except (AttributeError, OSError, TypeError, ValueError) as e: context.result.add_issue(f"Error checking `cite-as` downloadability: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_haspart.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_haspart.ttl index 95bffb33a..0293ea371 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_haspart.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_haspart.ttl @@ -19,27 +19,18 @@ ro-crate:RootDataEntityHasPartAllDataEntities a sh:NodeShape ; sh:name "Root Data Entity: hasPart MUST reference all Data Entities" ; sh:description """The Root Data Entity MUST directly or indirectly reference all Data Entities in the RO-Crate via hasPart (RO-Crate 1.2).""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?this a ro-crate:RootDataEntity . - } - """ - ] ; + sh:targetClass ro-crate:RootDataEntity ; sh:sparql [ a sh:SPARQLConstraint ; sh:prefixes ro-crate:sparqlPrefixes ; sh:select """ - SELECT $this ?unreferenced + SELECT $this ?value WHERE { $this a ro-crate:RootDataEntity . - ?unreferenced a ro-crate:DataEntity . - FILTER(?unreferenced != $this) + ?value a ro-crate:DataEntity . + FILTER(?value != $this) FILTER NOT EXISTS { - $this schema:hasPart+ ?unreferenced + $this schema:hasPart+ ?value } } """ ; diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_identifier.py b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_identifier.py index 5ac8766c1..2fe90ec38 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_identifier.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_identifier.py @@ -14,7 +14,8 @@ import re -from rocrate_validator.models import ValidationContext +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -28,10 +29,11 @@ class RootDataEntityIdentifierChecker(PyFunctionCheck): """ @check(name="Root Data Entity: REQUIRED value") - def check_identifier(self, context: ValidationContext) -> bool: + def check_identifier(self, context: ValidationContext) -> CheckResultValue: try: - if context.ro_crate.is_detached(): - return True + if not context.ro_crate.is_attached(): + context.record_skip(self, "attached package context is unavailable", "returned") + return CheckResult.SKIPPED root_entity = context.ro_crate.metadata.get_root_data_entity() if root_entity.id == "./": return True @@ -42,6 +44,10 @@ def check_identifier(self, context: ValidationContext) -> bool: self, ) return False - except Exception as e: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except (AttributeError, TypeError, ValueError) as e: context.result.add_issue(f"Error checking Root Data Entity @id: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_metadata.ttl index 68ce7aaf3..bc0a4690e 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_data_entity_metadata.ttl @@ -28,7 +28,10 @@ ro-crate:MetadataAboutTarget a sh:SPARQLTarget ; SELECT ?this WHERE { ?metadatafile schema:about ?this . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) } """ . @@ -46,7 +49,10 @@ ro-crate:FindRootDataEntity a sh:NodeShape, validator:HiddenShape ; WHERE { ?this a schema:Dataset . ?metadatafile schema:about ?this . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) } """ ] ; diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_identifier_property.py b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_identifier_property.py index 1c1f17b9f..501ac7eb2 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/2_root_identifier_property.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/2_root_identifier_property.py @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from rocrate_validator.models import ValidationContext +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -27,7 +28,7 @@ class RootIdentifierPropertyChecker(PyFunctionCheck): """ @check(name="Root Data Entity: identifier PropertyValue value") - def check_identifier_values(self, context: ValidationContext) -> bool: + def check_identifier_values(self, context: ValidationContext) -> CheckResultValue: try: root = context.ro_crate.metadata.get_root_data_entity() identifiers = root.get_property("identifier") @@ -43,6 +44,10 @@ def check_identifier_values(self, context: ValidationContext) -> bool: context.result.add_issue("PropertyValue identifiers MUST include a `value`", self) return False return True - except Exception as e: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except (AttributeError, TypeError, ValueError) as e: context.result.add_issue(f"Error checking identifier PropertyValue: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py index 780ab17f1..51daccd35 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.py @@ -14,16 +14,52 @@ import contextlib import re +from pathlib import Path +from urllib.parse import unquote, urlsplit -from rocrate_validator.models import ValidationContext +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.signposting import check_downloadable +from rocrate_validator.utils.uri import is_external_reference # set up logging logger = logging.getLogger(__name__) +def _data_entity_path_error(identifier: str) -> str | None: + """ + Validate path forms without inferring transport or payload membership. + + This is not a general URI validator. RFC 8089 requires an absolute path + after ``file:``, whereas RFC 3986 network-path references have an authority + but no scheme. Neither a network-path reference nor a bare filesystem path + expresses a path relative to the crate root. + """ + if "\\" in identifier or " " in identifier: + return "has an invalid @id; use URI-compatible paths" + # Match the existing URI utility's drive-letter disambiguation on every OS. + if re.match(r"^[A-Za-z]:/", identifier): + return "has a Windows drive path; use a relative URI path or an absolute file URI" + try: + parsed = urlsplit(identifier) + except ValueError: + return "has an invalid @id; use a valid URI reference" + if parsed.scheme == "file" and not parsed.path.startswith("/"): + return "has an invalid @id; the file URI path MUST be absolute (RFC 8089 section 2)" + path_error = None + if not parsed.scheme: + if identifier.startswith("//"): + path_error = ( + "has a network-path reference; use an absolute URI with a scheme " + "or a path relative to the RO-Crate root" + ) + elif parsed.path.startswith("/"): + path_error = "MUST use a relative @id within the RO-Crate root" + return path_error + + @requirement(name="Data Entity: REQUIRED resource availability") class DataEntityRequiredChecker(PyFunctionCheck): """ @@ -31,20 +67,28 @@ class DataEntityRequiredChecker(PyFunctionCheck): """ @check(name="Data Entity: REQUIRED resource availability") - def check_availability(self, context: ValidationContext) -> bool: + def check_availability(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 """ Check the presence of the Data Entity in the RO-Crate """ - if context.ro_crate.is_detached(): - logger.debug("Skipping data entity payload checks for detached RO-Crate") - return True + if not context.ro_crate.is_attached(): + logger.debug("Skipping data entity payload checks without an attached package context") + context.record_skip(self, "attached package context is unavailable", "returned") + return CheckResult.SKIPPED # Skip the check in metadata-only mode if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") - return True + context.record_skip(self, "metadata-only mode", "configured") + return CheckResult.SKIPPED # Perform the check result = True - for entity in context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True): + try: + entities = context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity availability check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + for entity in entities: assert entity.id is not None, "Entity has no @id" logger.debug("Ensure the presence of the Data Entity '%s' within the RO-Crate", entity.id) try: @@ -59,7 +103,7 @@ def check_availability(self, context: ValidationContext) -> bool: entity.id, ) continue - if not entity.has_relative_path(): + if is_external_reference(entity.id) or not entity.has_relative_path(): logger.debug( "Ignoring the Data Entity '%s' as it is a local entity with an absolute path. " "According to the RO-Crate specification, local entities with absolute paths " @@ -73,7 +117,7 @@ def check_availability(self, context: ValidationContext) -> bool: f"The RO-Crate does not include the Data Entity '{entity.id}' as part of its payload", self ) result = False - except Exception as e: + except (AttributeError, OSError, TypeError, ValueError) as e: context.result.add_issue( f"Unable to check the the presence of the Data Entity '{entity.id}' within the RO-Crate", self ) @@ -93,17 +137,27 @@ class DetachedDataEntityChecker(PyFunctionCheck): """ @check(name="Detached RO-Crate: data entities MUST be web-based") - def check_detached_entities(self, context: ValidationContext) -> bool: + def check_detached_entities(self, context: ValidationContext) -> CheckResultValue: if not context.ro_crate.is_detached(): - return True + context.record_skip(self, "detached package context is unavailable", "returned") + return CheckResult.SKIPPED result = True root_entity_id = None - with contextlib.suppress(Exception): + with contextlib.suppress(ValueError): root_entity_id = context.ro_crate.metadata.get_root_data_entity().id - for entity in context.ro_crate.metadata.get_data_entities(): + try: + entities = context.ro_crate.metadata.get_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping detached Data Entity check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + for entity in entities: if root_entity_id and entity.id == root_entity_id: continue - if not entity.is_remote(): + if entity.id.startswith("#"): + # Contextual entities do not identify Detached package payloads. + continue + if not is_external_reference(entity.id): context.result.add_issue( f"Data Entity '{entity.id}' is not web-based, " f"but in a detached RO-Crate all Data Entities " @@ -123,70 +177,81 @@ class DataEntityIdentifierChecker(PyFunctionCheck): """ @check(name="Data Entity: @id value requirements") - def check_identifiers(self, context: ValidationContext) -> bool: + def check_identifiers(self, context: ValidationContext) -> CheckResultValue: result = True root_entity_id = None - root_entity_is_local = False - root_entity_absolute_path = None - with contextlib.suppress(Exception): - root_data_entity = context.ro_crate.metadata.get_root_data_entity() - root_entity_id = root_data_entity.id - root_entity_is_local = ( - root_data_entity.id_as_uri.is_local_resource() if root_data_entity.id_as_uri else False - ) - root_entity_absolute_path = root_data_entity.id_as_path if root_data_entity.has_absolute_path() else None - for entity in context.ro_crate.metadata.get_data_entities(): + with contextlib.suppress(ValueError): + root_entity_id = context.ro_crate.metadata.get_root_data_entity().id + try: + entities = context.ro_crate.metadata.get_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + for entity in entities: if root_entity_id and entity.id == root_entity_id: continue - if not root_entity_is_local and not entity.is_remote(): - context.result.add_issue( - f"Data Entity '{entity.id}' has a local identifier but the Root Data Entity " - "does not have a local identifier", - self, - ) - result = False - if context.fail_fast: - return False if entity.has_local_identifier(): continue - if "\\" in entity.id or " " in entity.id: - context.result.add_issue( - f"Data Entity '{entity.id}' has an invalid @id; use URI-compatible paths", self - ) - result = False - if context.fail_fast: - return False - if ( - root_entity_is_local - and not str(entity.id_as_path).startswith(str(root_entity_absolute_path)) - and not str(entity.id).startswith("./") - and (str(entity.id).startswith(("/", "file://"))) - ): - context.result.add_issue( - f"Data Entity '{entity.id}' MUST use a relative @id within the RO-Crate root", self - ) + path_error = _data_entity_path_error(entity.id) + if path_error: + context.result.add_issue(f"Data Entity '{entity.id}' {path_error}", self) result = False if context.fail_fast: return False return result @check(name="Data Entity: relative @id for payload files") - def check_relative_paths(self, context: ValidationContext) -> bool: - if context.ro_crate.is_detached(): - return True + def check_relative_paths(self, context: ValidationContext) -> CheckResultValue: + if not context.ro_crate.is_attached(): + context.record_skip(self, "attached package context is unavailable", "returned") + return CheckResult.SKIPPED result = True - for entity in context.ro_crate.metadata.get_data_entities(): - if entity.has_local_identifier() or entity.is_remote(): - continue - if entity.has_absolute_path() and ( - context.ro_crate.has_file(entity.id_as_path) or context.ro_crate.has_directory(entity.id_as_path) + try: + entities = context.ro_crate.metadata.get_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping relative Data Entity identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + for entity in entities: + # This check only applies to attached crates with local filesystem + # resources; local IDs and malformed paths are handled elsewhere. + if ( + entity.has_local_identifier() + or _data_entity_path_error(entity.id) + or entity.is_remote() + or not context.ro_crate.uri.is_local_resource() ): - context.result.add_issue( - f"Data Entity '{entity.id}' should use a relative @id within the RO-Crate root", self - ) - result = False - if context.fail_fast: - return False + continue + # An absolute file URI may identify an external local resource. Only + # files actually inside the package must use a relative identifier. + # Parse URI paths explicitly to handle file:/ and localhost, and + # compare path components rather than textual prefixes. + if not (is_external_reference(entity.id) or entity.has_absolute_path()): + continue + + # URI paths are percent-encoded, so decode them before creating a + # filesystem path. Non-file absolute paths already have a path form. + parsed = urlsplit(entity.id) + path = Path(unquote(parsed.path)) if parsed.scheme == "file" else entity.id_as_path + # Resolve both paths before comparing them; this also follows symlinks, + # so containment is evaluated against their canonical filesystem targets. + root_path = context.ro_crate.uri.as_path().resolve() + path = path.resolve() + if not path.is_relative_to(root_path): + continue + + # An absolute path inside the root is only subject to this rule if + # it resolves to an actual file or directory in the crate payload. + payload_path = path.relative_to(root_path) + if not (context.ro_crate.has_file(payload_path) or context.ro_crate.has_directory(payload_path)): + continue + context.result.add_issue( + f"Data Entity '{entity.id}' MUST use a relative @id within the RO-Crate root", self + ) + result = False + if context.fail_fast: + return False return result @@ -197,9 +262,15 @@ class DataEntityCitationChecker(PyFunctionCheck): """ @check(name="Data Entity: citation must include @id") - def check_citation(self, context: ValidationContext) -> bool: + def check_citation(self, context: ValidationContext) -> CheckResultValue: result = True - for entity in context.ro_crate.metadata.get_data_entities(): + try: + entities = context.ro_crate.metadata.get_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity citation check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + for entity in entities: citations = entity.get_property("citation") if citations is None: continue @@ -248,26 +319,33 @@ def _not_downloadable_message(entity_id: str, dl) -> str: return msg @check(name="Web-based Data Entity: REQUIRED resource availability") - def check_availability(self, context: ValidationContext) -> bool: + def check_availability(self, context: ValidationContext) -> CheckResultValue: if ( context.settings.skip_availability_check or not (context.settings.creation_time or context.settings.enforce_availability) or context.settings.metadata_only ): - return True + context.record_skip(self, "availability check is disabled or not requested", "configured") + return CheckResult.SKIPPED result = True - for entity in context.ro_crate.metadata.get_web_data_entities(): + try: + entities = context.ro_crate.metadata.get_web_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping web-based Data Entity availability check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + for entity in entities: assert entity.id is not None, "Entity has no @id" # Skip directory URIs: assumed available, not directly downloadable by spec if entity.id.endswith("/"): logger.debug("Skipping downloadability check for directory entity '%s'", entity.id) continue try: - dl = check_downloadable(entity.id) + dl = check_downloadable(entity.id_as_uri.uri) if not dl.is_downloadable: context.result.add_issue(self._not_downloadable_message(entity.id, dl), self) result = False - except Exception as e: + except (OSError, RuntimeError, TypeError, ValueError) as e: context.result.add_issue(f"Web-based Data Entity '{entity.id}' availability check failed: {e}", self) result = False if not result and context.fail_fast: diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.ttl index ada5c742c..f69df9a19 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/4_data_entity_metadata.ttl @@ -20,7 +20,7 @@ @prefix validator: . ro-crate:DataEntityRequiredProperties a sh:NodeShape ; - sh:name "Data Entity: REQUIRED properties" ; + sh:name "Data Entity: REQUIRED @id format" ; sh:description """A Data Entity MUST be a `URI Path` relative to the ROCrate root, or an absolute URI""" ; sh:targetClass ro-crate:DataEntity ; @@ -44,9 +44,14 @@ ro-crate:FileDataEntity a sh:NodeShape ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) + } } ?this a schema:MediaObject . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) } """ @@ -93,10 +98,15 @@ ro-crate:DirectoryDataEntity a sh:NodeShape ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) + } } ?this a schema:Dataset . - ?metadatafile schema:about ?root . # Exclude all dataset entities that ends with `./#` - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER(?this != ?root) FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) } @@ -138,8 +148,8 @@ ro-crate:DirectoryDataEntity a sh:NodeShape ; ] . ro-crate:DataEntityRequiredPropertiesShape a sh:NodeShape ; - sh:name "Data Entity: REQUIRED properties" ; - sh:description """A `DataEntity`The file descriptor MUST be a valid JSON-LD file MUST be linked, either directly or indirectly, from the Root Data Entity""" ; + sh:name "Data Entity: REQUIRED Root linkage" ; + sh:description """A `DataEntity` MUST be linked, either directly or indirectly, from the Root Data Entity""" ; sh:targetClass ro-crate:DataEntity ; sh:property [ @@ -161,9 +171,14 @@ ro-crate:GenericDataEntityRequiredProperties a sh:NodeShape ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root ?metadatafile WHERE { + ?metadatafile schema:about ?root . + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) + } } ?root schema:hasPart ?this . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER(?this != ?root) FILTER(?this != ?metadatafile) FILTER NOT EXISTS { diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/5_web_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/5_web_data_entity_metadata.ttl index db63676ad..68d31785f 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/5_web_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/5_web_data_entity_metadata.ttl @@ -34,7 +34,10 @@ ro-crate:WebBasedDataEntity a sh:NodeShape, validator:HiddenShape ; SELECT ?this WHERE { ?this a schema:MediaObject . - FILTER(!contains(str(?this), "ro-crate-metadata.json")) + FILTER( + !STRENDS(str(?this), "ro-crate-metadata.json") && + !STRENDS(str(?this), "ro-crate-metadata.jsonld") + ) FILTER regex(str(?this), "^(https?|ftps?)://", "i") } """ diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/6_contextual_entity_metadata.ttl index 80168a0b1..e913d6d1b 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/6_contextual_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/6_contextual_entity_metadata.ttl @@ -19,31 +19,27 @@ @prefix schema_org: . @prefix sh: . @prefix validator: . +@prefix validator-transformers: . @prefix xsd: . ro-crate:ContextualEntityDefinition a sh:NodeShape, validator:HiddenShape ; + validator:requiresGraphTransformer validator-transformers:validationCandidateMarker ; sh:name "Identify Contextual Entities" ; sh:description """Mark entities as Contextual Entities if they are in the RO-Crate metadata but are not Data Entities, not the Root Data Entity, - and not the Metadata File Descriptor. The query uses `?this ?p ?o` - (subject of any triple) β€” SHACL rules from earlier shapes (e.g. - CreativeWorkAuthorDefinition) tag referenced-but-undescribed entities - with rdf:type, so they appear as subjects and are therefore captured. - Vocabulary namespaces loaded from ontologies (RDF/RDFS/OWL/XSD, - schema.org, bioschemas, w3id.org/ro, …) are excluded at the query - level; domain-specific legacy filters that wrongly excluded legitimate - crate identifiers (iana, urn, foaf, …) have been removed.""" ; + and not the Metadata File Descriptor. Validation candidates are + classified from asserted source triples before ontology inoculation, + inference, or SHACL rules. This includes type-only and claim-only + entities while excluding cited vocabulary terms.""" ; sh:target [ a sh:SPARQLTarget ; sh:prefixes ro-crate:sparqlPrefixes ; sh:select """ - SELECT DISTINCT ?this + SELECT ?this WHERE { - # Structural criteria: IRI or blank node, subject of some triple. - # SHACL rules from earlier shapes (e.g. CreativeWorkAuthorDefinition) - # tag even referenced-but-undescribed entities so they become - # subjects and are correctly classified as Contextual Entities. - ?this ?p ?o . + # Candidates were classified from the asserted source graph, + # before ontology inoculation, inference, and SHACL rules. + ?this validator-transformers:validationCandidate true . FILTER(isIRI(?this) || isBlank(?this)) # Structural exclusions β€” Metadata Descriptor, Root Data Entity, @@ -55,33 +51,6 @@ ro-crate:ContextualEntityDefinition a sh:NodeShape, validator:HiddenShape ; FILTER NOT EXISTS { ?this a schema:Dataset } FILTER NOT EXISTS { ?this a owl:Ontology } - # Vocabulary / profile namespace exclusions. Required because - # the RO-Crate 1.2 profile loads ontology.ttl into the same data - # graph as the metadata file; terms from those vocabularies - # appear as subjects with rdf:type + rdfs:label and would - # otherwise be misclassified. - FILTER(!STRSTARTS(STR(?this), "http://www.w3.org/")) - FILTER(!STRSTARTS(STR(?this), "https://www.w3.org/")) - FILTER(!STRSTARTS(STR(?this), "http://schema.org/")) - FILTER(!STRSTARTS(STR(?this), "https://schema.org/")) - FILTER(!STRSTARTS(STR(?this), "http://purl.org/")) - FILTER(!STRSTARTS(STR(?this), "https://bioschemas.org/")) - FILTER(!STRSTARTS(STR(?this), "http://www.opengis.net/")) - FILTER(!STRSTARTS(STR(?this), "https://w3id.org/")) - FILTER(!STRSTARTS(STR(?this), "https://github.com/crs4/rocrate-validator/")) - FILTER(!STRSTARTS(STR(?this), "https://codemeta.github.io/")) - FILTER(!STRSTARTS(STR(?this), "http://pcdm.org/")) - FILTER(!STRSTARTS(STR(?this), "http://xmlns.com/foaf/")) - FILTER(!STRSTARTS(STR(?this), "http://creativecommons.org/ns")) - FILTER(!STRSTARTS(STR(?this), "http://www.iana.org/")) - FILTER(!STRSTARTS(STR(?this), "https://www.iana.org/")) - FILTER(!STRSTARTS(STR(?this), "https://nationaalarchief.nl/archieven/archief/")) - FILTER(!STRSTARTS(STR(?this), "https://www.nationalarchives.gov.uk")) - - # Narrow urn: exclusion β€” only the internal ontology versionIRI. - # Unlike the legacy blanket `urn:` filter, this keeps legitimate - # `urn:uuid:*` contextual entities (e.g. DigitalDocument) in play. - FILTER(!STRSTARTS(STR(?this), "urn:absolute:")) } """ ] ; diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/8_workflow_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/8_workflow_metadata.ttl index 3e1aa66e1..0c0741dcd 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/must/8_workflow_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/must/8_workflow_metadata.ttl @@ -20,23 +20,11 @@ @prefix xsd: . @prefix validator: . -# Selects every SoftwareSourceCode entity (Scripts and Workflows). -ro-crate:SoftwareSourceCodeTarget a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?this a schema:SoftwareSourceCode . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) - } - """ . - # --------------------------------------------------------------- # Identify Script entities # # A Script is an entity with schema:SoftwareSourceCode in @type -# that is NOT a Workflow (no bioschemas:ComputationalWorkflow). +# that is NOT a Workflow (`no bioschemas:ComputationalWorkflow). # --------------------------------------------------------------- ro-crate:ScriptDefinition a sh:NodeShape, validator:HiddenShape ; sh:name "Identify Script entities" ; @@ -49,8 +37,6 @@ ro-crate:ScriptDefinition a sh:NodeShape, validator:HiddenShape ; SELECT ?this WHERE { ?this a schema:SoftwareSourceCode . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER NOT EXISTS { ?this a bioschemas:ComputationalWorkflow } } """ @@ -72,18 +58,7 @@ ro-crate:WorkflowDefinition a sh:NodeShape, validator:HiddenShape ; sh:name "Identify Workflow entities" ; sh:description """Mark entities as Workflows if they have bioschemas:ComputationalWorkflow in @type.""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?this a bioschemas:ComputationalWorkflow . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) - } - """ - ] ; + sh:targetClass bioschemas:ComputationalWorkflow ; sh:rule [ a sh:TripleRule ; sh:subject sh:this ; @@ -161,7 +136,7 @@ ro-crate:ScriptOrWorkflowRequiredName a sh:NodeShape ; sh:name "Script or Workflow: REQUIRED `name`" ; sh:description """Scripts and Workflows MUST have a human-readable `name` property (RO-Crate 1.2, Workflows and Scripts).""" ; - sh:target ro-crate:SoftwareSourceCodeTarget ; + sh:targetClass schema:SoftwareSourceCode ; sh:property [ a sh:PropertyShape ; sh:name "Script or Workflow: REQUIRED `name` property" ; diff --git a/rocrate_validator/profiles/ro-crate/1.2/must/9_schema_org_iri_protocol.ttl b/rocrate_validator/profiles/ro-crate/1.2/must/9_schema_org_iri_protocol.ttl new file mode 100644 index 000000000..c1c54460c --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.2/must/9_schema_org_iri_protocol.ttl @@ -0,0 +1,45 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro-crate: . +@prefix sh: . + +ro-crate:SchemaOrgTypeProtocol a sh:NodeShape ; + sh:name "Schema.org @type protocol" ; + sh:description "Schema.org @type values MUST use the http://schema.org/ namespace defined by the RO-Crate context." ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT DISTINCT ?this + WHERE { + ?this a ?schemaType . + } + """ + ] ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT $this ?schemaType ?term ?httpType + WHERE { + $this a ?schemaType . + FILTER(STRSTARTS(STR(?schemaType), "https://schema.org/")) + BIND(SUBSTR(STR(?schemaType), STRLEN("https://schema.org/") + 1) AS ?term) + BIND(IRI(CONCAT("http://schema.org/", ?term)) AS ?httpType) + } + """ ; + sh:message "Entity {?this} declares the @type '{?schemaType}', which is not a term of the RO-Crate @context: the context maps Schema.org terms to 'http://schema.org/', so the entity MUST be typed '{?term}' (i.e. '{?httpType}')" ; + sh:severity sh:Violation + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.2/ontology.ttl b/rocrate_validator/profiles/ro-crate/1.2/ontology.ttl index 84331ccc7..bade971e6 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/ontology.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/ontology.ttl @@ -19,7 +19,7 @@ @prefix xsd: . @prefix rdfs: . @prefix schema: . -@prefix ro-crate: . +@prefix ro-crate: . @prefix bioschemas: . # @base <./.> . @@ -54,13 +54,13 @@ schema:SoftwareSourceCode rdf:type owl:Class ; bioschemas:ComputationalWorkflow rdf:type owl:Class . -### https://github.com/crs4/rocrate-validator/profiles/ro-crate/1.2/Script +### https://github.com/crs4/rocrate-validator/profiles/ro-crate-1.2/Script ro-crate:Script rdf:type owl:Class ; rdfs:subClassOf schema:SoftwareSourceCode, schema:MediaObject ; rdfs:label "Script"@en . -### https://github.com/crs4/rocrate-validator/profiles/ro-crate/1.2/Workflow +### https://github.com/crs4/rocrate-validator/profiles/ro-crate-1.2/Workflow ro-crate:Workflow rdf:type owl:Class ; rdfs:subClassOf schema:SoftwareSourceCode, schema:MediaObject, bioschemas:ComputationalWorkflow ; rdfs:label "Workflow"@en . diff --git a/rocrate_validator/profiles/ro-crate/1.2/prefixes.ttl b/rocrate_validator/profiles/ro-crate/1.2/prefixes.ttl index 8cad6b4dd..ffa42e3a6 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/prefixes.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/prefixes.ttl @@ -56,4 +56,10 @@ ro-crate:sparqlPrefixes sh:declare [ sh:declare [ sh:prefix "ro" ; sh:namespace "./"^^xsd:anyURI ; + ] ; + # Private predicates added to the transient validation graph by Python + # graph transformers. They are validator bookkeeping, not crate metadata. + sh:declare [ + sh:prefix "validator-transformers" ; + sh:namespace "https://github.com/crs4/rocrate-validator/graph-transformers/"^^xsd:anyURI ; ] . diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_identifier_format.py b/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_identifier_format.py index 6f9d8745b..c227d9684 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_identifier_format.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_identifier_format.py @@ -22,7 +22,8 @@ import contextlib import re -from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -46,13 +47,19 @@ class EntityIdentifierFormatChecker(PyFunctionCheck): """ @check(name="Entity identifiers SHOULD NOT use ../", severity=Severity.RECOMMENDED) - def check_no_parent_traversal(self, context: ValidationContext) -> bool: + def check_no_parent_traversal(self, context: ValidationContext) -> CheckResultValue: """ @id paths SHOULD NOT use `../` to climb out of the RO-Crate Root (RO-Crate 1.2, JSON-LD appendix). """ result = True - for entity in context.ro_crate.metadata.as_dict().get("@graph", []): + try: + entities = context.ro_crate.metadata.as_dict().get("@graph", []) + except ROCrateMetadataNotFoundError: + logger.debug("Skipping parent traversal check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + for entity in entities: entity_id = entity.get("@id", "") if "../" in entity_id: context.result.add_issue( @@ -69,13 +76,19 @@ def check_no_parent_traversal(self, context: ValidationContext) -> bool: name="Entity identifiers SHOULD use native UTF-8, not percent-encoding", severity=Severity.RECOMMENDED, ) - def check_utf8_identifiers(self, context: ValidationContext) -> bool: + def check_utf8_identifiers(self, context: ValidationContext) -> CheckResultValue: """ International characters in @id values SHOULD be written in native UTF-8 rather than percent-encoded (RO-Crate 1.2, JSON-LD appendix). """ result = True - for entity in context.ro_crate.metadata.as_dict().get("@graph", []): + try: + entities = context.ro_crate.metadata.as_dict().get("@graph", []) + except ROCrateMetadataNotFoundError: + logger.debug("Skipping entity identifier encoding check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + for entity in entities: entity_id = entity.get("@id", "") if _PCT_NON_ASCII_RE.search(entity_id): context.result.add_issue( @@ -93,7 +106,7 @@ def check_utf8_identifiers(self, context: ValidationContext) -> bool: name="Contextual entity @id SHOULD be absolute URI or '#'-prefixed", severity=Severity.RECOMMENDED, ) - def check_named_entity_id_format(self, context: ValidationContext) -> bool: + def check_named_entity_id_format(self, context: ValidationContext) -> CheckResultValue: """ Any Contextual Entity (Person, Organization, ContactPoint, PropertyValue, Place, etc.) SHOULD use an @id that is an absolute URI (permalink), @@ -103,14 +116,20 @@ def check_named_entity_id_format(self, context: ValidationContext) -> bool: result = True ro_crate_metadata = context.ro_crate.metadata non_contextual_ids = set() - with contextlib.suppress(Exception): + with contextlib.suppress(ValueError): non_contextual_ids.add(ro_crate_metadata.get_file_descriptor_entity().id) - with contextlib.suppress(Exception): + with contextlib.suppress(ValueError): non_contextual_ids.add(ro_crate_metadata.get_root_data_entity().id) - with contextlib.suppress(Exception): + with contextlib.suppress(ValueError): non_contextual_ids.update(e.id for e in ro_crate_metadata.get_data_entities()) - for entity in ro_crate_metadata.as_dict().get("@graph", []): + try: + entities = ro_crate_metadata.as_dict().get("@graph", []) + except ROCrateMetadataNotFoundError: + logger.debug("Skipping named entity identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + for entity in entities: entity_id = entity.get("@id", "") if entity_id in non_contextual_ids: continue diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl index 007c0f5cb..462270a3c 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/should/0_entity_metadata.ttl @@ -15,8 +15,11 @@ @prefix sh: . @prefix schema: . @prefix xsd: . +@prefix validator: . +@prefix validator-transformers: . ro-crate:ROCrateMetadataEntityRecommendedType a sh:NodeShape ; + validator:requiresGraphTransformer validator-transformers:validationCandidateMarker ; sh:name "RO-Crate Metadata Entity: RECOMMENDED properties" ; sh:description """Check if the RO-Crate Metadata Entity includes at least one Schema.org type and a human-readable name""" ; sh:target [ @@ -25,24 +28,24 @@ ro-crate:ROCrateMetadataEntityRecommendedType a sh:NodeShape ; sh:select """ SELECT ?this WHERE { + ?this validator-transformers:validationCandidate true . ?this a ?type . - # Exclude Root and RO-Crate Metadata File entities - ?root a schema:Dataset . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + { SELECT DISTINCT ?root ?metadatafile WHERE { + ?root a schema:Dataset . + ?metadatafile schema:about ?root . + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) + } } FILTER(?this != ?metadatafile) FILTER (?this != ?root) - # Exclude entities with non-IRI identifiers or those from specific namespaces + # Exclude entities with non-IRI identifiers. FILTER (isIRI(?this)) - FILTER(!STRSTARTS(STR(?this), "http://www.w3.org/")) - FILTER(!STRSTARTS(STR(?this), "https://w3id.org/ro/crate/")) - FILTER(!STRSTARTS(STR(?this), "http://schema.org/")) - FILTER(!STRSTARTS(STR(?this), "https://schema.org/")) - FILTER(!STRSTARTS(STR(?this), "http://purl.org/")) - FILTER(!STRSTARTS(STR(?this), "https://bioschemas.org/")) - FILTER(!STRSTARTS(STR(?this), "https://github.com/crs4/rocrate-validator/")) - FILTER(!STRSTARTS(STR(?this), "urn:")) + # W3C Profile Vocabulary descriptors are not ordinary Schema.org + # metadata entities; they have dedicated profile-specific rules. + FILTER NOT EXISTS { ?this a } } """ ; ] ; diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/1_file_descriptor_name.py b/rocrate_validator/profiles/ro-crate/1.2/should/1_file_descriptor_name.py index e7034f3d1..4261a1ed2 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/1_file_descriptor_name.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/1_file_descriptor_name.py @@ -13,7 +13,7 @@ # limitations under the License. -from rocrate_validator.models import ValidationContext +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -29,7 +29,7 @@ class FileDescriptorExistence(PyFunctionCheck): """ @check(name="Detached RO-Crate file descriptor RECOMMENDED naming convention") - def test_detached_descriptor_filename(self, context: ValidationContext) -> bool: + def test_detached_descriptor_filename(self, context: ValidationContext) -> CheckResultValue: """ Check if the file descriptor of a Detached RO-Crate exists and is named according to the convention. In a Detached RO-Crate, the file descriptor SHOULD be named `{prefix}-ro-crate-metadata.json`, @@ -37,7 +37,8 @@ def test_detached_descriptor_filename(self, context: ValidationContext) -> bool: """ if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") - return True + context.record_skip(self, "metadata-only mode", "configured") + return CheckResult.SKIPPED if not context.ro_crate.has_descriptor(): message = f'file descriptor "{context.rel_fd_path}" is not present' context.result.add_issue(message, self) diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/2_root_data_entity_identifier.py b/rocrate_validator/profiles/ro-crate/1.2/should/2_root_data_entity_identifier.py index c957b475a..c17ee903a 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/2_root_data_entity_identifier.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/2_root_data_entity_identifier.py @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.signposting import check_downloadable @@ -28,16 +29,18 @@ class DetachedROCrateRootDataEntityIdentifierChecker(PyFunctionCheck): """ @check(name="Root Data Entity: RECOMMENDED identifier") - def check_identifier(self, context: ValidationContext) -> bool: + def check_identifier(self, context: ValidationContext) -> CheckResultValue: # noqa: PLR0911 """ In a detached RO-Crate, the Root Data Entity @id SHOULD be an absolute URI. """ try: if not context.ro_crate.is_detached(): - return True + context.record_skip(self, "detached package context is unavailable", "returned") + return CheckResult.SKIPPED root_entity = context.ro_crate.metadata.get_root_data_entity() if not root_entity.is_remote(): - return True + context.record_skip(self, "root data entity is not remote", "returned") + return CheckResult.SKIPPED if root_entity.id == "./": context.result.add_issue( @@ -51,7 +54,11 @@ def check_identifier(self, context: ValidationContext) -> bool: return False return True - except Exception as e: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Root Data Entity identifier check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except (AttributeError, TypeError, ValueError) as e: context.result.add_issue(f"Error checking Root Data Entity @id: {e!s}", self) return False @@ -64,17 +71,19 @@ class RootDataEntityCiteAsIdentifierChecker(PyFunctionCheck): """ @check(name="Root Data Entity: use cite-as for resolvable identifiers") - def check_cite_as_reference(self, context: ValidationContext) -> bool: + def check_cite_as_reference(self, context: ValidationContext) -> CheckResultValue: # noqa: PLR0911 """ If the Root Data Entity has a resolvable identifier, it SHOULD be included in the `cite-as` property of the RO-Crate Metadata Entity. """ try: if not context.ro_crate.is_detached(): - return True + context.record_skip(self, "detached package context is unavailable", "returned") + return CheckResult.SKIPPED root_entity = context.ro_crate.metadata.get_root_data_entity() if not root_entity.is_remote(): - return True + context.record_skip(self, "root data entity is not remote", "returned") + return CheckResult.SKIPPED # Check if the `cite-as` property is present and references the Root Data Entity cite_as = root_entity.get_property("cite-as") @@ -96,7 +105,11 @@ def check_cite_as_reference(self, context: ValidationContext) -> bool: return False return True - except Exception as e: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Root Data Entity cite-as check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except (AttributeError, TypeError, ValueError) as e: context.result.add_issue(f"Error checking Root Data Entity `cite-as` reference: {e!s}", self) return False @@ -140,11 +153,13 @@ class RootDataEntityPersistentIdentifierChecker(PyFunctionCheck): """ @check(name="Root Data Entity: identifier SHOULD resolve to RO-Crate content", severity=Severity.RECOMMENDED) - def check_identifier_resolvable(self, context: ValidationContext) -> bool: + def check_identifier_resolvable(self, context: ValidationContext) -> CheckResultValue: # noqa: PLR0911 if context.settings.skip_availability_check: - return True + context.record_skip(self, "availability checks are disabled", "configured") + return CheckResult.SKIPPED if context.settings.metadata_only: - return True + context.record_skip(self, "metadata-only mode", "configured") + return CheckResult.SKIPPED try: root_entity = context.ro_crate.metadata.get_root_data_entity() identifier_raw = root_entity.get_property("identifier") @@ -169,6 +184,10 @@ def check_identifier_resolvable(self, context: ValidationContext) -> bool: context.result.add_issue(msg, self) result = False return result - except Exception as e: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Root Data Entity identifier resolution check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except (AttributeError, OSError, TypeError, ValueError) as e: context.result.add_issue(f"Error checking Root Data Entity identifier resolution: {e!s}", self) return False diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/3_entity_property_compact_representation_1_2.py b/rocrate_validator/profiles/ro-crate/1.2/should/3_entity_property_compact_representation_1_2.py new file mode 100644 index 000000000..53b0db0b7 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.2/should/3_entity_property_compact_representation_1_2.py @@ -0,0 +1,59 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) + +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging +from rocrate_validator.utils.jsonld import find_singleton_property_arrays + +logger = logging.getLogger(__name__) + + +@requirement(name="Entity properties compact representation") +class EntityPropertyCompactRepresentation(PyFunctionCheck): + """ + Single-element arrays used for entity properties SHOULD be unpacked to a + single value in compacted RO-Crate 1.2 JSON-LD. + """ + + @check(name="Entity properties SHOULD use single values rather than singleton arrays") + def check_singleton_property_arrays(self, context: ValidationContext) -> bool: + """Check the RO-Crate 1.2 recommendation for singleton property arrays.""" + try: + findings = find_singleton_property_arrays(context.ro_crate.metadata.as_dict()) + except ROCrateMetadataNotFoundError as error: + logger.debug("Skipping singleton property array check: metadata descriptor is not available (%s)", error) + return True + except (AttributeError, TypeError, ValueError) as error: + # Required format checks report malformed metadata structure. + logger.debug("Skipping singleton property array check: metadata cannot be inspected (%s)", error) + return True + + result = True + for entity_id, property_name in findings: + context.result.add_issue( + f'Entity "{entity_id}" property "{property_name}" SHOULD be represented as a single value, ' + "not a singleton array, according to the RO-Crate 1.2 JSON-LD recommendation", + self, + violatingEntity=entity_id, + violatingProperty=property_name, + violatingPropertyValue="singleton array", + ) + result = False + if context.fail_fast: + return result + return result diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_license.py b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_license.py index 2ab81a7bc..050b16650 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_license.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_license.py @@ -46,7 +46,8 @@ # check provides actionable feedback to authors without penalising # valid crates. -from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -100,12 +101,17 @@ class DataEntityLicenseDivergenceChecker(PyFunctionCheck): """ @check(name="Data Entity SHOULD NOT redundantly declare the Root license", severity=Severity.RECOMMENDED) - def check_license_divergence(self, context: ValidationContext) -> bool: + def check_license_divergence(self, context: ValidationContext) -> CheckResultValue: # noqa: C901, PLR0911 root_entity = None try: root_entity = context.ro_crate.metadata.get_root_data_entity() - except Exception: - return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity license check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except ValueError: + context.record_skip(self, "root data entity is not available", "exception") + return CheckResult.SKIPPED if root_entity is None: return True @@ -117,7 +123,13 @@ def check_license_divergence(self, context: ValidationContext) -> bool: if not root_license_id: return True - for entity in context.ro_crate.metadata.get_data_entities(): + try: + entities = context.ro_crate.metadata.get_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity license check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + for entity in entities: if entity.id == root_entity.id: continue entity_license_raw = entity.get_property("license") diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.py b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.py index 713cf47fe..28642a2fd 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.py @@ -14,7 +14,8 @@ import re -from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.signposting import check_downloadable @@ -30,9 +31,15 @@ class DataEntityCitationChecker(PyFunctionCheck): """ @check(name="Data Entity: citation must include @id") - def check_citation(self, context: ValidationContext) -> bool: + def check_citation(self, context: ValidationContext) -> CheckResultValue: result = True - for entity in context.ro_crate.metadata.get_data_entities(): + try: + entities = context.ro_crate.metadata.get_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Data Entity citation check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + for entity in entities: citations = entity.get_property("citation") if citations is None: continue @@ -81,25 +88,32 @@ def _not_downloadable_message(entity_id: str, dl) -> str: return msg @check(name="Web-based Data Entity: RECOMMENDED resource availability", severity=Severity.RECOMMENDED) - def check_availability_warning(self, context: ValidationContext) -> bool: + def check_availability_warning(self, context: ValidationContext) -> CheckResultValue: if ( context.settings.skip_availability_check or context.settings.creation_time or context.settings.enforce_availability or context.settings.metadata_only ): - return True + context.record_skip(self, "availability check is disabled or not applicable", "configured") + return CheckResult.SKIPPED result = True - for entity in context.ro_crate.metadata.get_web_data_entities(): + try: + entities = context.ro_crate.metadata.get_web_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping web-based Data Entity availability check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + for entity in entities: assert entity.id is not None, "Entity has no @id" if entity.id.endswith("/"): continue try: - dl = check_downloadable(entity.id) + dl = check_downloadable(entity.id_as_uri.uri) if not dl.is_downloadable: context.result.add_issue(self._not_downloadable_message(entity.id, dl), self) result = False - except Exception as e: + except (OSError, RuntimeError, TypeError, ValueError) as e: context.result.add_issue(f"Web-based Data Entity '{entity.id}' availability check failed: {e}", self) result = False if not result and context.fail_fast: @@ -107,11 +121,18 @@ def check_availability_warning(self, context: ValidationContext) -> bool: return result @check(name="Web-based Data Entity: `contentSize` property", severity=Severity.RECOMMENDED) - def check_content_size(self, context: ValidationContext) -> bool: + def check_content_size(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 if context.settings.skip_availability_check: - return True + context.record_skip(self, "availability checks are disabled", "configured") + return CheckResult.SKIPPED result = True - for entity in context.ro_crate.metadata.get_web_data_entities(): + try: + entities = context.ro_crate.metadata.get_web_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping web-based Data Entity content size check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + for entity in entities: assert entity.id is not None, "Entity has no @id" if entity.is_available(): content_size = entity.get_property("contentSize") @@ -124,9 +145,9 @@ def check_content_size(self, context: ValidationContext) -> bool: content_value = str(content_size) try: content_int = int(str(content_value)) - except Exception: + except (TypeError, ValueError, OverflowError): content_int = None - external_size = context.ro_crate.get_external_file_size(entity.id) + external_size = context.ro_crate.get_external_file_size(entity.id_as_uri.uri) if external_size is not None and content_int is not None and content_int != external_size: context.result.add_issue( f"The property contentSize={content_size} of the Web-based Data Entity " @@ -143,11 +164,18 @@ def check_content_size(self, context: ValidationContext) -> bool: return result @check(name="Web-based Data Entity: `contentUrl` availability", severity=Severity.RECOMMENDED) - def check_content_url(self, context: ValidationContext) -> bool: + def check_content_url(self, context: ValidationContext) -> CheckResultValue: # noqa: C901 if context.settings.skip_availability_check: - return True + context.record_skip(self, "availability checks are disabled", "configured") + return CheckResult.SKIPPED result = True - for entity in context.ro_crate.metadata.get_web_data_entities(): + try: + entities = context.ro_crate.metadata.get_web_data_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping web-based Data Entity content URL check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + for entity in entities: content_url = entity.get_property("contentUrl") if not content_url: continue @@ -165,7 +193,7 @@ def check_content_url(self, context: ValidationContext) -> bool: msg += f": {dl.reason}" context.result.add_issue(msg, self) result = False - except Exception as e: + except (OSError, RuntimeError, TypeError, ValueError) as e: context.result.add_issue( f"contentUrl '{url_value}' for Web-based Data Entity '{entity.id}' " f"availability check failed: {e}", diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.ttl index bcb836835..d098d2abc 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_data_entity_metadata.ttl @@ -164,9 +164,14 @@ ro-crate:WebDirectoryDistributionRecommended a sh:NodeShape ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) + } } ?this a schema:Dataset . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER(?this != ?root) FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) FILTER regex(str(?this), "^https?://", "i") diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_dataset_data_entity.ttl b/rocrate_validator/profiles/ro-crate/1.2/should/4_dataset_data_entity.ttl index d46263091..951bf4dcf 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_dataset_data_entity.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_dataset_data_entity.ttl @@ -21,9 +21,14 @@ ro-crate:LocalDatasetTarget a sh:SPARQLTarget ; sh:select """ SELECT ?this WHERE { + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) + } } ?this a schema:Dataset . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) FILTER(?this != ?root) FILTER(!STRSTARTS(STR(?this), CONCAT(STR(?root), "#"))) FILTER(!regex(str(?this), "^https?://", "i")) diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_missing_file_local_path.py b/rocrate_validator/profiles/ro-crate/1.2/should/4_missing_file_local_path.py index 08bbe75ce..b1d9a8693 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_missing_file_local_path.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_missing_file_local_path.py @@ -32,7 +32,8 @@ import contextlib -from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -99,14 +100,21 @@ def _check_entity_local_path(self, context: ValidationContext, entity) -> bool: return True @check(name="Missing local File SHOULD use localPath", severity=Severity.RECOMMENDED) - def check_missing_file_local_path(self, context: ValidationContext) -> bool: - if context.ro_crate.is_detached() or context.settings.metadata_only: - return True + def check_missing_file_local_path(self, context: ValidationContext) -> CheckResultValue: + if not context.ro_crate.is_attached() or context.settings.metadata_only: + context.record_skip(self, "attached payload context is unavailable", "configured") + return CheckResult.SKIPPED root_entity_id = None - with contextlib.suppress(Exception): + with contextlib.suppress(ValueError): root_entity_id = context.ro_crate.metadata.get_root_data_entity().id result = True - for entity in context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True): + try: + entities = context.ro_crate.metadata.get_data_entities(exclude_web_data_entities=True) + except ROCrateMetadataNotFoundError: + logger.debug("Skipping missing local file check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + for entity in entities: if root_entity_id and entity.id == root_entity_id: continue if not entity.is_file(): diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.py b/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.py index 377d0fe0f..2bbf7f492 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.py @@ -23,7 +23,8 @@ This Python check performs the network-dependent refinement. """ -from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.signposting import has_signposting_cite_as @@ -61,7 +62,7 @@ def _needs_sddatepublished_check(entity, root_id) -> bool: name="Referenced RO-Crate: `sdDatePublished` SHOULD be present when Signposting cite-as is not declared", severity=Severity.RECOMMENDED, ) - def check_sddatepublished_signposting(self, context: ValidationContext) -> bool: + def check_sddatepublished_signposting(self, context: ValidationContext) -> CheckResultValue: """ For each referenced RO-Crate data entity whose @id is an absolute URI with no declared `identifier` and no `sdDatePublished`, verify whether @@ -73,15 +74,27 @@ def check_sddatepublished_signposting(self, context: ValidationContext) -> bool: or not (context.settings.creation_time or context.settings.enforce_availability) or context.settings.metadata_only ): - return True + context.record_skip(self, "availability check is disabled or not applicable", "configured") + return CheckResult.SKIPPED result = True try: root = context.ro_crate.metadata.get_root_data_entity() - except Exception: - return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping referenced RO-Crate check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except ValueError: + context.record_skip(self, "root data entity is not available", "exception") + return CheckResult.SKIPPED - for entity in context.ro_crate.metadata.get_dataset_entities(): + try: + entities = context.ro_crate.metadata.get_dataset_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping referenced RO-Crate check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + for entity in entities: if not self._needs_sddatepublished_check(entity, root.id): continue entity_id = entity.id diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.ttl b/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.ttl index 26a96b3a2..4b1138d71 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_referenced_rocrate.ttl @@ -35,8 +35,13 @@ ro-crate:ReferencedROCrateDataEntityTarget a sh:SPARQLTarget ; ?this a schema:Dataset ; dct:conformsTo ?profile . FILTER(STRSTARTS(STR(?profile), "https://w3id.org/ro/crate")) - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) + } } FILTER(?this != ?root) } """ . @@ -56,8 +61,13 @@ ro-crate:ReferencedROCrateMetadataDescriptorTarget a sh:SPARQLTarget ; dct:conformsTo ?profile ; schema:subjectOf ?this . FILTER(STRSTARTS(STR(?profile), "https://w3id.org/ro/crate")) - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + { SELECT DISTINCT ?root WHERE { + ?metadatafile schema:about ?root . + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) + } } FILTER(?crate != ?root) } """ . diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/4_web_dataset_distribution.py b/rocrate_validator/profiles/ro-crate/1.2/should/4_web_dataset_distribution.py index 810b1646c..58013d0a8 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/4_web_dataset_distribution.py +++ b/rocrate_validator/profiles/ro-crate/1.2/should/4_web_dataset_distribution.py @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from rocrate_validator.models import Severity, ValidationContext +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, Severity, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging from rocrate_validator.utils.signposting import check_downloadable @@ -71,7 +72,7 @@ def _check_distribution(self, context: ValidationContext, entity_id, dist) -> bo msg += f": {dl.reason}" context.result.add_issue(msg, self) return False - except Exception as e: + except (OSError, RuntimeError, TypeError, ValueError) as e: context.result.add_issue( f"Error checking downloadability of distribution '{url}' for Dataset '{entity_id}': {e}", self ) @@ -79,11 +80,18 @@ def _check_distribution(self, context: ValidationContext, entity_id, dist) -> bo return True @check(name="Dataset: distribution SHOULD be downloadable", severity=Severity.RECOMMENDED) - def check_distribution_downloadable(self, context: ValidationContext) -> bool: + def check_distribution_downloadable(self, context: ValidationContext) -> CheckResultValue: if context.settings.skip_availability_check or context.settings.metadata_only: - return True + context.record_skip(self, "availability checks are disabled or metadata-only mode is active", "configured") + return CheckResult.SKIPPED result = True - for entity in context.ro_crate.metadata.get_dataset_entities(): + try: + entities = context.ro_crate.metadata.get_dataset_entities() + except ROCrateMetadataNotFoundError: + logger.debug("Skipping Dataset distribution check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + for entity in entities: distribution_raw = entity.get_property("distribution") if not distribution_raw: continue diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/6_contextual_entity_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/should/6_contextual_entity_metadata.ttl index 3103fffc3..387d9fa19 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/6_contextual_entity_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/should/6_contextual_entity_metadata.ttl @@ -162,16 +162,7 @@ ro-crate:PersonAffiliationRecommendedProperties a sh:NodeShape ; ro-crate:LicenseEntityRecommendedProperties a sh:NodeShape ; sh:name "License entity: RECOMMENDED properties" ; sh:description """License entities SHOULD have `name` and `description`.""" ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes ro-crate:sparqlPrefixes ; - sh:select """ - SELECT ?this - WHERE { - ?subject schema:license ?this . - } - """ - ] ; + sh:targetObjectsOf schema:license ; sh:property [ sh:path schema:name ; sh:minCount 1 ; diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/8_workflow_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.2/should/8_workflow_metadata.ttl index c4a9aeebd..f7bc6a397 100644 --- a/rocrate_validator/profiles/ro-crate/1.2/should/8_workflow_metadata.ttl +++ b/rocrate_validator/profiles/ro-crate/1.2/should/8_workflow_metadata.ttl @@ -29,8 +29,6 @@ ro-crate:ScriptOrWorkflowImageTarget a sh:SPARQLTarget ; WHERE { ?sw a schema:SoftwareSourceCode . ?sw schema:image ?this . - ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) } """ . @@ -43,7 +41,7 @@ ro-crate:ScriptOrWorkflowProgrammingLanguage a sh:NodeShape ; sh:description """Scripts and Workflows SHOULD have a `programmingLanguage` property referencing a contextual entity of type `ComputerLanguage` (RO-Crate 1.2, Workflows and Scripts).""" ; - sh:target ro-crate:SoftwareSourceCodeTarget ; + sh:targetClass schema:SoftwareSourceCode ; sh:property [ a sh:PropertyShape ; sh:name "Script or Workflow: RECOMMENDED `programmingLanguage` property" ; diff --git a/rocrate_validator/profiles/ro-crate/1.2/should/9_schema_org_id_compatibility.ttl b/rocrate_validator/profiles/ro-crate/1.2/should/9_schema_org_id_compatibility.ttl new file mode 100644 index 000000000..6e0460103 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.2/should/9_schema_org_id_compatibility.ttl @@ -0,0 +1,58 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro-crate: . +@prefix sh: . + +ro-crate:SchemaOrgIdCompatibility a sh:NodeShape ; + sh:name "Schema.org @id compatibility" ; + sh:description "Nested Schema.org @id references SHOULD use the http://schema.org/ namespace for compatibility with the RO-Crate context." ; + # OWL-RL adds reflexive owl:sameAs triples for IRIs; these are not nested + # @id references and must not produce compatibility recommendations. + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT DISTINCT ?this + WHERE { + ?this ?predicate ?schemaId . + FILTER(?predicate NOT IN ( + , + + )) + FILTER(isIRI(?schemaId)) + FILTER(STRSTARTS(STR(?schemaId), "https://schema.org/")) + } + """ + ] ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT $this ?predicate ?schemaId ?term ?httpId + WHERE { + $this ?predicate ?schemaId . + FILTER(?predicate NOT IN ( + , + + )) + FILTER(isIRI(?schemaId)) + FILTER(STRSTARTS(STR(?schemaId), "https://schema.org/")) + BIND(SUBSTR(STR(?schemaId), STRLEN("https://schema.org/") + 1) AS ?term) + BIND(IRI(CONCAT("http://schema.org/", ?term)) AS ?httpId) + } + """ ; + sh:message "Entity {?this} references the Schema.org IRI '{?schemaId}' through a nested @id. For compatibility with the RO-Crate @context, the reference SHOULD use '{?term}' (i.e. '{?httpId}')" ; + sh:severity sh:Warning + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_context.py b/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_context.py new file mode 100644 index 000000000..3985f4986 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/must/0_file_descriptor_context.py @@ -0,0 +1,114 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re +from http import HTTPStatus +from typing import Any +from urllib.parse import urljoin + +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging +from rocrate_validator.utils.http import HttpRequester, OfflineCacheMissError + +logger = logging.getLogger(__name__) + +_HANDLED_ERRORS = (AssertionError, AttributeError, KeyError, TypeError, ValueError) + + +@requirement(name="File Descriptor JSON-LD format") +class FileDescriptorJsonLdContext(PyFunctionCheck): + """Validate the RO-Crate 1.3 JSON-LD context reference.""" + + def __get_remote_context__(self, context_uri: str) -> object: + raw_data = HttpRequester().get(context_uri, headers={"Accept": "application/ld+json, application/json"}) + if raw_data.status_code != HTTPStatus.OK: + raise RuntimeError(f"Unable to retrieve the JSON-LD context '{context_uri}'") + + content_type = raw_data.headers.get("Content-Type", "") + if "application/ld+json" not in content_type and "application/json" not in content_type: + link_header = raw_data.headers.get("Link", "") + match = re.search(r'<([^>]+)>;\s*rel="alternate";\s*type="application/(ld\+json|json)"', link_header) + if not match: + raise RuntimeError(f"Unable to retrieve the JSON-LD context from '{context_uri}'") + alternate_url = match.group(1) + if not alternate_url.startswith("http"): + alternate_url = urljoin(context_uri, alternate_url) + raw_data = HttpRequester().get( + alternate_url, + headers={"Accept": "application/ld+json, application/json"}, + ) + if raw_data.status_code != HTTPStatus.OK: + raise RuntimeError(f"Unable to retrieve the JSON-LD context from alternate URL '{alternate_url}'") + + jsonld_context = raw_data.json()["@context"] + assert isinstance(jsonld_context, dict) + return jsonld_context + + def __check_remote_context__(self, context_uri: str) -> bool: + try: + return isinstance(self.__get_remote_context__(context_uri), dict) + except OfflineCacheMissError: + raise + except (OSError, RuntimeError, *_HANDLED_ERRORS): + logger.exception("Unexpected error during file descriptor context validation") + return False + + def __check_contexts__(self, context: ValidationContext, jsonld_context: object) -> bool: + is_valid = True + if isinstance(jsonld_context, str) and not self.__check_remote_context__(jsonld_context): + context.result.add_issue(f'Unable to retrieve the JSON-LD context "{jsonld_context}"', self) + is_valid = False + if isinstance(jsonld_context, list): + for nested_context in jsonld_context: + if not self.__check_contexts__(context, nested_context): + is_valid = False + return is_valid + + @check( + name="File Descriptor @context property validation", + depends_on=("File Descriptor JSON format",), + ) + def check_context(self, context: ValidationContext) -> CheckResultValue: + """Require the RO-Crate 1.3 JSON-LD context and validate it.""" + expected_context = "https://w3id.org/ro/crate/1.3/context" + try: + json_dict: dict[str, Any] = context.ro_crate.metadata.as_dict() + jsonld_context = json_dict.get("@context") + if "@context" not in json_dict: + context.result.add_issue( + f'RO-Crate file descriptor "{context.rel_fd_path}" does not contain a context', self + ) + return False + if not ( + jsonld_context == expected_context + or (isinstance(jsonld_context, list) and expected_context in jsonld_context) + ): + context.result.add_issue( + f'RO-Crate file descriptor "{context.rel_fd_path}" ' + f'does not reference the required context "{expected_context}"', + self, + ) + return False + return self.__check_contexts__(context, jsonld_context) + except ROCrateMetadataNotFoundError: + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except UnicodeDecodeError: + context.record_skip(self, "descriptor encoding check reported the failure", "exception") + return CheckResult.SKIPPED + except _HANDLED_ERRORS: + logger.exception("Unexpected error during file descriptor context validation") + return False diff --git a/rocrate_validator/profiles/ro-crate/1.3/must/8_workflow_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/must/8_workflow_metadata.ttl new file mode 100644 index 000000000..f96123efa --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/must/8_workflow_metadata.ttl @@ -0,0 +1,150 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +@prefix ro: <./> . +@prefix ro-crate: . +@prefix rdf: . +@prefix schema: . +@prefix bioschemas: . +@prefix sh: . +@prefix xsd: . +@prefix validator: . + +# --------------------------------------------------------------- +# Identify Script entities +# +# A Script is an entity with schema:SoftwareSourceCode in @type +# that is NOT a Workflow (`no bioschemas:ComputationalWorkflow). +# --------------------------------------------------------------- +ro-crate:ScriptDefinition a sh:NodeShape, validator:HiddenShape ; + sh:name "Identify Script entities" ; + sh:description """Mark entities as Scripts if they have schema:SoftwareSourceCode + in @type but NOT bioschemas:ComputationalWorkflow.""" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?this a schema:SoftwareSourceCode . + FILTER NOT EXISTS { ?this a bioschemas:ComputationalWorkflow } + } + """ + ] ; + sh:rule [ + a sh:TripleRule ; + sh:subject sh:this ; + sh:predicate rdf:type ; + sh:object ro-crate:Script ; + ] . + +# --------------------------------------------------------------- +# Identify Workflow entities +# +# A Workflow is an entity with bioschemas:ComputationalWorkflow +# in @type. +# --------------------------------------------------------------- +ro-crate:WorkflowDefinition a sh:NodeShape, validator:HiddenShape ; + sh:name "Identify Workflow entities" ; + sh:description """Mark entities as Workflows if they have + bioschemas:ComputationalWorkflow in @type.""" ; + sh:targetClass bioschemas:ComputationalWorkflow ; + sh:rule [ + a sh:TripleRule ; + sh:subject sh:this ; + sh:predicate rdf:type ; + sh:object ro-crate:Workflow ; + ] . + +# --------------------------------------------------------------- +# Script: MUST include `File` (schema:MediaObject) in @type +# --------------------------------------------------------------- +ro-crate:ScriptRequiredFileType a sh:NodeShape ; + sh:name "Script: REQUIRED `File` type" ; + sh:description """A Script entity MUST include `File` (schema:MediaObject) + in its @type alongside `SoftwareSourceCode` + (RO-Crate 1.3, Workflows and Scripts β€” Script).""" ; + sh:targetClass ro-crate:Script ; + sh:property [ + a sh:PropertyShape ; + sh:name "Script: REQUIRED `File` type" ; + sh:description """Check that the Script entity includes `File` + (schema:MediaObject) in its @type.""" ; + sh:path rdf:type ; + sh:hasValue schema:MediaObject ; + sh:minCount 1 ; + sh:severity sh:Violation ; + sh:message "A Script MUST include `File` in its `@type` (schema:MediaObject / schema:SoftwareSourceCode)" ; + ] . + +# --------------------------------------------------------------- +# Workflow: MUST include `File` (schema:MediaObject) in @type +# --------------------------------------------------------------- +ro-crate:WorkflowRequiredFileType a sh:NodeShape ; + sh:name "Workflow: REQUIRED `File` type" ; + sh:description """A Workflow entity MUST include `File` (schema:MediaObject) + in its @type alongside `SoftwareSourceCode` and `ComputationalWorkflow` + (RO-Crate 1.3, Workflows and Scripts β€” Workflow).""" ; + sh:targetClass ro-crate:Workflow ; + sh:property [ + a sh:PropertyShape ; + sh:name "Workflow: REQUIRED `File` type" ; + sh:description """Check that the Workflow entity includes `File` + (schema:MediaObject) in its @type.""" ; + sh:path rdf:type ; + sh:hasValue schema:MediaObject ; + sh:minCount 1 ; + sh:severity sh:Violation ; + sh:message "A Workflow MUST include `File` in its `@type` (schema:MediaObject / schema:SoftwareSourceCode / bioschemas:ComputationalWorkflow)" ; + ] . + +# --------------------------------------------------------------- +# Workflow: MUST include `SoftwareSourceCode` in @type +# --------------------------------------------------------------- +ro-crate:WorkflowRequiredSoftwareSourceCodeType a sh:NodeShape ; + sh:name "Workflow: REQUIRED `SoftwareSourceCode` type" ; + sh:description """A Workflow entity MUST include `SoftwareSourceCode` + in its @type alongside `File` and `ComputationalWorkflow` + (RO-Crate 1.3, Workflows and Scripts β€” Workflow).""" ; + sh:targetClass ro-crate:Workflow ; + sh:property [ + a sh:PropertyShape ; + sh:name "Workflow: REQUIRED `SoftwareSourceCode` type" ; + sh:description """Check that the Workflow entity includes + `SoftwareSourceCode` in its @type.""" ; + sh:path rdf:type ; + sh:hasValue schema:SoftwareSourceCode ; + sh:minCount 1 ; + sh:severity sh:Violation ; + sh:message "A Workflow MUST include `SoftwareSourceCode` in its `@type` (schema:MediaObject / schema:SoftwareSourceCode / bioschemas:ComputationalWorkflow)" ; + ] . + +# --------------------------------------------------------------- +# Script or Workflow: MUST have a `name` property +# --------------------------------------------------------------- +ro-crate:ScriptOrWorkflowRequiredName a sh:NodeShape ; + sh:name "Script or Workflow: REQUIRED `name`" ; + sh:description """Scripts and Workflows MUST have a human-readable `name` + property (RO-Crate 1.3, Workflows and Scripts).""" ; + sh:targetClass schema:SoftwareSourceCode ; + sh:property [ + a sh:PropertyShape ; + sh:name "Script or Workflow: REQUIRED `name` property" ; + sh:description """Check that the Script or Workflow entity has a + human-readable `name` property.""" ; + sh:path schema:name ; + sh:minCount 1 ; + sh:nodeKind sh:Literal ; + sh:severity sh:Violation ; + sh:message "Scripts and Workflows MUST have a human-readable `name` property" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/ontology.ttl b/rocrate_validator/profiles/ro-crate/1.3/ontology.ttl new file mode 100644 index 000000000..ce194f0d7 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/ontology.ttl @@ -0,0 +1,93 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix owl: . +@prefix rdf: . +@prefix xml: . +@prefix xsd: . +@prefix rdfs: . +@prefix schema: . +@prefix ro-crate: . +@prefix ro-crate-1.2: . +@prefix bioschemas: . +# @base <./.> . + + rdf:type owl:Ontology ; + owl:versionIRI . + +# The descriptor discovery rule is inherited from the 1.2 overlay source and +# assigns its internal marker class. Both marker IRIs denote the same +# validator concept, so OWL inference makes the inherited marker available to +# shapes written against the native 1.3 vocabulary. +ro-crate:ROCrateMetadataFileDescriptor + owl:equivalentClass ro-crate-1.2:ROCrateMetadataFileDescriptor . + +# # ################################################################# +# # # Classes +# # ################################################################# + +# Declare the RootDataEntity class +ro-crate:RootDataEntity rdf:type owl:Class ; + owl:equivalentClass ro-crate-1.2:RootDataEntity ; + rdfs:subClassOf schema:Dataset ; + rdfs:label "RootDataEntity"@en . + +### http://schema.org/CreativeWork +schema:CreativeWork rdf:type owl:Class ; + rdfs:label "CreativeWork"@en . + +### http://schema.org/MediaObject +schema:MediaObject rdf:type owl:Class ; + owl:equivalentClass ro-crate:File ; + rdfs:label "MediaObject"@en . + + +### http://schema.org/SoftwareSourceCode +schema:SoftwareSourceCode rdf:type owl:Class ; + rdfs:subClassOf schema:CreativeWork . + + +### https://bioschemas.org/terms/ComputationalWorkflow +bioschemas:ComputationalWorkflow rdf:type owl:Class . + + +### https://github.com/crs4/rocrate-validator/profiles/ro-crate-1.3/Script +ro-crate:Script rdf:type owl:Class ; + rdfs:subClassOf schema:SoftwareSourceCode, schema:MediaObject ; + rdfs:label "Script"@en . + + +### https://github.com/crs4/rocrate-validator/profiles/ro-crate-1.3/Workflow +ro-crate:Workflow rdf:type owl:Class ; + rdfs:subClassOf schema:SoftwareSourceCode, schema:MediaObject, bioschemas:ComputationalWorkflow ; + rdfs:label "Workflow"@en . + + +### https://w3id.org/ro/crate/1.3/DataEntity +ro-crate:DataEntity rdf:type owl:Class ; + rdfs:subClassOf schema:CreativeWork ; + rdfs:label "DataEntity"@en . + + +# # ### https://w3id.org/ro/crate/1.3/Directory +ro-crate:Directory rdf:type owl:Class ; + rdfs:subClassOf schema:Dataset ; + rdfs:label "Directory"@en . + + +### https://w3id.org/ro/crate/1.3/ContextualEntity +ro-crate:ContextualEntity rdf:type owl:Class ; + rdfs:subClassOf schema:Thing ; + rdfs:label "ContextualEntity"@en . diff --git a/rocrate_validator/profiles/ro-crate/1.3/prefixes.ttl b/rocrate_validator/profiles/ro-crate/1.3/prefixes.ttl new file mode 100644 index 000000000..f6f8da2c0 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/prefixes.ttl @@ -0,0 +1,55 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix sh: . +@prefix xsd: . +@prefix ro-crate: . + +# Define the prefixes used in the SPARQL queries +ro-crate:sparqlPrefixes sh:declare [ + sh:prefix "schema" ; + sh:namespace "http://schema.org/"^^xsd:anyURI ; +] ; + sh:declare [ + sh:prefix "bioschemas" ; + sh:namespace "https://bioschemas.org/terms/"^^xsd:anyURI ; + ] ; + sh:declare [ + sh:prefix "rocrate" ; + sh:namespace "https://w3id.org/ro/crate/1.3/"^^xsd:anyURI ; + ] ; + sh:declare [ + sh:prefix "wfrun" ; + sh:namespace "https://w3id.org/ro/terms/workflow-run#"^^xsd:anyURI ; + ] ; + sh:declare [ + sh:prefix "codemeta" ; + sh:namespace "https://codemeta.github.io/terms/"^^xsd:anyURI ; + ] ; + sh:declare [ + sh:prefix "iana-relation" ; + sh:namespace "http://www.iana.org/assignments/relation/"^^xsd:anyURI ; + ] ; + sh:declare [ + sh:prefix "ro-crate" ; + sh:namespace "https://github.com/crs4/rocrate-validator/profiles/ro-crate-1.3/"^^xsd:anyURI ; + ] ; + sh:declare [ + sh:prefix "dct" ; + sh:namespace "http://purl.org/dc/terms/"^^xsd:anyURI ; + ] ; + sh:declare [ + sh:prefix "ro" ; + sh:namespace "./"^^xsd:anyURI ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/profile.ttl b/rocrate_validator/profiles/ro-crate/1.3/profile.ttl new file mode 100644 index 000000000..30579e6bb --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/profile.ttl @@ -0,0 +1,80 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix dct: . +@prefix prof: . +@prefix role: . +@prefix rdfs: . +@prefix validator: . + + + + # a Profile; it's identifying URI + a prof:Profile ; + + # common metadata for the Profile + + # the Profile's label + rdfs:label "RO-Crate Metadata Specification 1.3" ; + + # regular metadata, a basic description of the Profile + rdfs:comment """RO-Crate Metadata Specification."""@en ; + + # regular metadata, URI of publisher + dct:publisher ; + + # Reuse the RO-Crate 1.2 validation rules and override only the rules whose + # behavior changes because of the RO-Crate 1.3 JSON-LD context. + prof:isProfileOf ; + validator:isRuleOverlayOf ; + + # this profile has a JSON-LD context resource + prof:hasResource [ + a prof:ResourceDescriptor ; + + # it's in JSON-LD format + dct:format ; + + # it conforms to JSON-LD, here referred to by its namespace URI as a Profile + dct:conformsTo ; + + # this profile resource plays the role of "Vocabulary" + # described in this ontology's accompanying Roles vocabulary + prof:hasRole role:Vocabulary ; + + # this profile resource's actual file + prof:hasArtifact ; + ] ; + + # this profile has a human-readable documentation resource + prof:hasResource [ + a prof:ResourceDescriptor ; + + # it's in HTML format + dct:format ; + + # it conforms to HTML, here referred to by its namespace URI as a Profile + dct:conformsTo ; + + # this profile resource plays the role of "Specification" + # described in this ontology's accompanying Roles vocabulary + prof:hasRole role:Specification ; + + # this profile resource's actual file + prof:hasArtifact ; + ] ; + + # a short code to refer to the Profile with when a URI can't be used + prof:hasToken "ro-crate" ; +. diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/1_file-descriptor_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/1_file-descriptor_metadata.ttl new file mode 100644 index 000000000..3620c9567 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/1_file-descriptor_metadata.ttl @@ -0,0 +1,56 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix dct: . +@prefix sh: . + +ro-crate:ROCrateMetadataFileDescriptorConformsTo + a sh:NodeShape ; + sh:name "RO-Crate Metadata File Descriptor RECOMMENDED conformsTo" ; + sh:description """The RO-Crate Metadata Descriptor SHOULD indicate the versioned RO-Crate specification URI. + In RO-Crate 1.3, conformsTo SHOULD have a single value which is a versioned permalink URI + of the RO-Crate specification that the RO-Crate JSON-LD conforms to. + The URI SHOULD start with https://w3id.org/ro/crate/.""" ; + sh:targetClass ro-crate:ROCrateMetadataFileDescriptor ; + sh:property [ + a sh:PropertyShape ; + sh:minCount 1 ; + sh:name "Metadata File Descriptor: RECOMMENDED `conformsTo` value" ; + sh:description """Check if the RO-Crate Metadata File Descriptor has a versioned RO-Crate specification URI""" ; + sh:severity sh:Warning ; + sh:nodeKind sh:IRI ; + sh:path dct:conformsTo ; + sh:hasValue ; + sh:message "The RO-Crate metadata file descriptor SHOULD have a `conformsTo` property with the RO-Crate specification version" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Metadata File Descriptor: RECOMMENDED single `conformsTo` value" ; + sh:description """Check if the RO-Crate Metadata File Descriptor has a single `conformsTo` value""" ; + sh:severity sh:Warning ; + sh:path dct:conformsTo ; + sh:maxCount 1 ; + sh:message "The RO-Crate metadata file descriptor SHOULD have a single `conformsTo` value" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Metadata File Descriptor: RECOMMENDED `conformsTo` URI prefix" ; + sh:description """Check if the `conformsTo` URI starts with https://w3id.org/ro/crate/""" ; + sh:severity sh:Warning ; + sh:path dct:conformsTo ; + sh:pattern "^https://w3id\\.org/ro/crate/" ; + sh:message "The RO-Crate metadata file descriptor `conformsTo` URI SHOULD start with https://w3id.org/ro/crate/" ; + ] . diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/3_entity_property_compact_representation_1_3.py b/rocrate_validator/profiles/ro-crate/1.3/should/3_entity_property_compact_representation_1_3.py new file mode 100644 index 000000000..12e382026 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/3_entity_property_compact_representation_1_3.py @@ -0,0 +1,59 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) + +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging +from rocrate_validator.utils.jsonld import find_singleton_property_arrays + +logger = logging.getLogger(__name__) + + +@requirement(name="Entity properties compact representation") +class EntityPropertyCompactRepresentation(PyFunctionCheck): + """ + Single-element arrays used for entity properties SHOULD be unpacked to a + single value in compacted RO-Crate 1.3 JSON-LD. + """ + + @check(name="Entity properties SHOULD use single values rather than singleton arrays") + def check_singleton_property_arrays(self, context: ValidationContext) -> bool: + """Check the RO-Crate 1.3 recommendation for singleton property arrays.""" + try: + findings = find_singleton_property_arrays(context.ro_crate.metadata.as_dict()) + except ROCrateMetadataNotFoundError as error: + logger.debug("Skipping singleton property array check: metadata descriptor is not available (%s)", error) + return True + except (AttributeError, TypeError, ValueError) as error: + # Required format checks report malformed metadata structure. + logger.debug("Skipping singleton property array check: metadata cannot be inspected (%s)", error) + return True + + result = True + for entity_id, property_name in findings: + context.result.add_issue( + f'Entity "{entity_id}" property "{property_name}" SHOULD be represented as a single value, ' + "not a singleton array, according to the RO-Crate 1.3 JSON-LD recommendation", + self, + violatingEntity=entity_id, + violatingProperty=property_name, + violatingPropertyValue="singleton array", + ) + result = False + if context.fail_fast: + return result + return result diff --git a/rocrate_validator/profiles/ro-crate/1.3/should/8_workflow_metadata.ttl b/rocrate_validator/profiles/ro-crate/1.3/should/8_workflow_metadata.ttl new file mode 100644 index 000000000..1dfa19811 --- /dev/null +++ b/rocrate_validator/profiles/ro-crate/1.3/should/8_workflow_metadata.ttl @@ -0,0 +1,164 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +@prefix ro: <./> . +@prefix ro-crate: . +@prefix rdf: . +@prefix schema: . +@prefix bioschemas: . +@prefix dct: . +@prefix sh: . +@prefix xsd: . +@prefix validator: . + +# Selects ImageObject entities referenced via `image` from a Script or Workflow. +ro-crate:ScriptOrWorkflowImageTarget a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?sw a schema:SoftwareSourceCode . + ?sw schema:image ?this . + } + """ . + +# --------------------------------------------------------------- +# Script or Workflow: programmingLanguage SHOULD reference a +# ComputerLanguage entity +# --------------------------------------------------------------- +ro-crate:ScriptOrWorkflowProgrammingLanguage a sh:NodeShape ; + sh:name "Script or Workflow: RECOMMENDED `programmingLanguage`" ; + sh:description """Scripts and Workflows SHOULD have a `programmingLanguage` + property referencing a contextual entity of type `ComputerLanguage` + (RO-Crate 1.3, Workflows and Scripts).""" ; + sh:targetClass schema:SoftwareSourceCode ; + sh:property [ + a sh:PropertyShape ; + sh:name "Script or Workflow: RECOMMENDED `programmingLanguage` property" ; + sh:description """Check that the Script or Workflow has a `programmingLanguage` + property.""" ; + sh:path schema:programmingLanguage ; + sh:minCount 1 ; + sh:severity sh:Warning ; + sh:message "Scripts and Workflows SHOULD have a `programmingLanguage` property referencing a `ComputerLanguage` entity" ; + ] ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:name "Script or Workflow: `programmingLanguage` SHOULD reference ComputerLanguage" ; + sh:description """Check that the `programmingLanguage` property references + an entity of type `ComputerLanguage`.""" ; + sh:select """ + SELECT ?this + WHERE { + ?this schema:programmingLanguage ?lang . + FILTER NOT EXISTS { ?lang a schema:ComputerLanguage } + } + """ ; + sh:severity sh:Warning ; + sh:message "The `programmingLanguage` of a Script or Workflow SHOULD reference a `ComputerLanguage` entity" ; + ] . + +# --------------------------------------------------------------- +# Workflow: conformsTo SHOULD include a versioned Bioschemas +# ComputationalWorkflow profile URI +# --------------------------------------------------------------- +ro-crate:WorkflowRecommendedConformsTo a sh:NodeShape ; + sh:name "Workflow: RECOMMENDED Bioschemas `conformsTo`" ; + sh:description """A Workflow SHOULD declare `conformsTo` pointing to a + versioned Bioschemas ComputationalWorkflow profile URI + (https://bioschemas.org/profiles/ComputationalWorkflow/) + (RO-Crate 1.3, Workflows and Scripts).""" ; + sh:targetClass ro-crate:Workflow ; + sh:property [ + a sh:PropertyShape ; + sh:name "Workflow: RECOMMENDED `conformsTo` Bioschemas profile" ; + sh:description """Check that the Workflow has a `conformsTo` pointing + to a Bioschemas ComputationalWorkflow profile URI.""" ; + sh:path dct:conformsTo ; + sh:minCount 1 ; + sh:severity sh:Warning ; + sh:message "Workflows SHOULD declare `conformsTo` referencing a versioned Bioschemas ComputationalWorkflow profile URI (https://bioschemas.org/profiles/ComputationalWorkflow/)" ; + ] ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:name "Workflow `conformsTo` SHOULD reference Bioschemas profile" ; + sh:description """If `conformsTo` is present, at least one value SHOULD + start with the Bioschemas ComputationalWorkflow profile URI.""" ; + sh:select """ + SELECT ?this + WHERE { + ?this dct:conformsTo ?profile . + FILTER NOT EXISTS { + ?this dct:conformsTo ?bioprofile . + FILTER(STRSTARTS(STR(?bioprofile), "https://bioschemas.org/profiles/ComputationalWorkflow/")) + } + } + """ ; + sh:severity sh:Warning ; + sh:message "Workflow `conformsTo` SHOULD include a versioned Bioschemas ComputationalWorkflow profile URI (https://bioschemas.org/profiles/ComputationalWorkflow/)" ; + ] . + +# --------------------------------------------------------------- +# ImageObject linked from Script/Workflow: encodingFormat SHOULD +# be present +# --------------------------------------------------------------- +ro-crate:ScriptOrWorkflowImageEncodingFormat a sh:NodeShape ; + sh:name "Script/Workflow ImageObject: RECOMMENDED `encodingFormat`" ; + sh:description """ImageObject entities referenced via `image` from a + Script or Workflow SHOULD have an `encodingFormat` property with an + IANA media type string and/or a Pronom identifier reference + (RO-Crate 1.3, Workflows and Scripts β€” related ImageObject).""" ; + sh:target ro-crate:ScriptOrWorkflowImageTarget ; + sh:property [ + a sh:PropertyShape ; + sh:name "Script/Workflow ImageObject: RECOMMENDED `encodingFormat` property" ; + sh:description """Check that the ImageObject has an `encodingFormat` + property.""" ; + sh:path schema:encodingFormat ; + sh:minCount 1 ; + sh:severity sh:Warning ; + sh:message "An ImageObject referenced via `image` from a Script or Workflow SHOULD have an `encodingFormat` property (IANA media type and/or Pronom identifier)" ; + ] . + +# --------------------------------------------------------------- +# ImageObject linked from Script/Workflow: `about` SHOULD +# reference the script/workflow +# --------------------------------------------------------------- +ro-crate:ScriptOrWorkflowImageAbout a sh:NodeShape ; + sh:name "Script/Workflow ImageObject: RECOMMENDED `about` reference" ; + sh:description """ImageObject entities referenced via `image` from a + Script or Workflow SHOULD have an `about` property that references + the script or workflow entity + (RO-Crate 1.3, Workflows and Scripts β€” related ImageObject).""" ; + sh:target ro-crate:ScriptOrWorkflowImageTarget ; + sh:sparql [ + a sh:SPARQLConstraint ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:name "Script/Workflow ImageObject: `about` SHOULD reference the script/workflow" ; + sh:description """Check that the ImageObject's `about` property references + the script or workflow that includes it via `image`.""" ; + sh:select """ + SELECT ?this + WHERE { + ?sw a schema:SoftwareSourceCode . + ?sw schema:image ?this . + FILTER NOT EXISTS { + ?this schema:about ?sw . + } + } + """ ; + sh:severity sh:Warning ; + sh:message "An ImageObject referenced via `image` from a Script or Workflow SHOULD have an `about` property referencing the script or workflow" ; + ] . diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.0/may/0_main-workflow.ttl similarity index 100% rename from rocrate_validator/profiles/workflow-ro-crate/may/0_main-workflow.ttl rename to rocrate_validator/profiles/workflow-ro-crate/1.0/may/0_main-workflow.ttl diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py b/rocrate_validator/profiles/workflow-ro-crate/1.0/may/1_main_workflow.py similarity index 72% rename from rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py rename to rocrate_validator/profiles/workflow-ro-crate/1.0/may/1_main_workflow.py index e88e19837..6766d188f 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/may/1_main_workflow.py +++ b/rocrate_validator/profiles/workflow-ro-crate/1.0/may/1_main_workflow.py @@ -14,7 +14,8 @@ # pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) -from rocrate_validator.models import ValidationContext +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -27,11 +28,12 @@ class WorkflowFilesExistence(PyFunctionCheck): """Checks for workflow-related crate files existence.""" @check(name="Workflow diagram existence") - def check_workflow_diagram(self, context: ValidationContext) -> bool: + def check_workflow_diagram(self, context: ValidationContext) -> CheckResultValue: """Check if the crate contains the workflow diagram.""" if context.settings.metadata_only: logger.debug("Skipping file descriptor existence check in metadata-only mode") - return True + context.record_skip(self, "metadata-only mode", "configured") + return CheckResult.SKIPPED try: main_workflow = context.ro_crate.metadata.get_main_workflow() @@ -44,13 +46,18 @@ def check_workflow_diagram(self, context: ValidationContext) -> bool: context.result.add_issue(f"Workflow diagram '{image.id}' not found in crate", self) return False return True - except Exception: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping workflow diagram check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except (AttributeError, OSError, TypeError, ValueError) as e: + context.result.add_issue(f"Unable to check the workflow diagram: {e}", self) if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error checking main workflow image existence") return False @check(name="Workflow description existence") - def check_workflow_description(self, context: ValidationContext) -> bool: + def check_workflow_description(self, context: ValidationContext) -> CheckResultValue: """Check if the crate contains the workflow CWL description.""" try: main_workflow = context.ro_crate.metadata.get_main_workflow() @@ -65,7 +72,12 @@ def check_workflow_description(self, context: ValidationContext) -> bool: ) return False return True - except Exception: + except ROCrateMetadataNotFoundError: + logger.debug("Skipping workflow description check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except (AttributeError, OSError, TypeError, ValueError) as e: + context.result.add_issue(f"Unable to check the workflow description: {e}", self) if logger.isEnabledFor(logging.DEBUG): logger.exception("Unexpected error checking workflow description existence") return False diff --git a/rocrate_validator/profiles/workflow-ro-crate/may/2_wrroc_crate.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.0/may/2_wrroc_crate.ttl similarity index 100% rename from rocrate_validator/profiles/workflow-ro-crate/may/2_wrroc_crate.ttl rename to rocrate_validator/profiles/workflow-ro-crate/1.0/may/2_wrroc_crate.ttl diff --git a/rocrate_validator/profiles/workflow-ro-crate/must/0_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.0/must/0_main-workflow.ttl similarity index 94% rename from rocrate_validator/profiles/workflow-ro-crate/must/0_main-workflow.ttl rename to rocrate_validator/profiles/workflow-ro-crate/1.0/must/0_main-workflow.ttl index aefb42e7a..b3b161f2b 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/must/0_main-workflow.ttl +++ b/rocrate_validator/profiles/workflow-ro-crate/1.0/must/0_main-workflow.ttl @@ -45,7 +45,10 @@ workflow-ro-crate:FindMainWorkflow a sh:NodeShape, validator:HiddenShape ; ?root schema:mainEntity ?this . ?root a schema:Dataset . ?metadatafile schema:about ?root . - FILTER(contains(str(?metadatafile), "ro-crate-metadata.json")) + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) } """ ] ; diff --git a/rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py b/rocrate_validator/profiles/workflow-ro-crate/1.0/must/0_main_workflow.py similarity index 63% rename from rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py rename to rocrate_validator/profiles/workflow-ro-crate/1.0/must/0_main_workflow.py index 4e6beaf33..86501fbb4 100644 --- a/rocrate_validator/profiles/workflow-ro-crate/must/0_main_workflow.py +++ b/rocrate_validator/profiles/workflow-ro-crate/1.0/must/0_main_workflow.py @@ -14,7 +14,8 @@ # pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) -from rocrate_validator.models import ValidationContext +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement from rocrate_validator.utils import log as logging @@ -27,7 +28,7 @@ class MainWorkflowFileExistence(PyFunctionCheck): """Checks for main workflow file existence.""" @check(name="Main Workflow file must exist") - def check_workflow(self, context: ValidationContext) -> bool: + def check_workflow(self, context: ValidationContext) -> CheckResultValue: """Check if the crate contains the main workflow file.""" try: main_workflow = context.ro_crate.metadata.get_main_workflow() @@ -38,12 +39,19 @@ def check_workflow(self, context: ValidationContext) -> bool: context.result.add_issue(f"Main Workflow {main_workflow.id} not found in crate", self) return False return True - except ValueError: - context.result.add_issue( - "Unable to check the existence of the main workflow file " - "because the metadata file descriptor doesn't contain a `mainEntity`", - self, - ) + except ROCrateMetadataNotFoundError: + logger.debug("Skipping main workflow check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except (TypeError, ValueError) as error: + if str(error) == "no main workflow in metadata file descriptor": + message = ( + "Unable to check the existence of the main workflow file " + "because the metadata file descriptor doesn't contain a `mainEntity`" + ) + else: + message = f"Unable to check the existence of the main workflow file: {error}" + context.result.add_issue(message, self) if logger.isEnabledFor(logging.DEBUG): logger.exception("Unable to check main workflow file existence") return False diff --git a/rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.0/must/1_wroc_root_data_entity.ttl similarity index 100% rename from rocrate_validator/profiles/workflow-ro-crate/must/1_wroc_root_data_entity.ttl rename to rocrate_validator/profiles/workflow-ro-crate/1.0/must/1_wroc_root_data_entity.ttl diff --git a/rocrate_validator/profiles/workflow-ro-crate/profile.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.0/profile.ttl similarity index 100% rename from rocrate_validator/profiles/workflow-ro-crate/profile.ttl rename to rocrate_validator/profiles/workflow-ro-crate/1.0/profile.ttl diff --git a/rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.0/should/1_wroc_crate.ttl similarity index 100% rename from rocrate_validator/profiles/workflow-ro-crate/should/1_wroc_crate.ttl rename to rocrate_validator/profiles/workflow-ro-crate/1.0/should/1_wroc_crate.ttl diff --git a/rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.0/should/2_main-workflow.ttl similarity index 100% rename from rocrate_validator/profiles/workflow-ro-crate/should/2_main-workflow.ttl rename to rocrate_validator/profiles/workflow-ro-crate/1.0/should/2_main-workflow.ttl diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/may/0_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/0_main-workflow.ttl new file mode 100644 index 000000000..92af82be3 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/0_main-workflow.ttl @@ -0,0 +1,79 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix workflow-ro-crate: . +@prefix rdf: . +@prefix schema: . +@prefix bioschemas: . +@prefix sh: . +@prefix wroc: . + +workflow-ro-crate:MainWorkflowOptionalProperties a sh:NodeShape ; + sh:name "Main Workflow optional properties" ; + sh:description """Main Workflow properties defined as MAY"""; + sh:targetClass workflow-ro-crate:MainWorkflow ; + sh:property [ + a sh:PropertyShape ; + sh:name "Main Workflow image" ; + sh:description "The Crate MAY contain a Main Workflow Diagram; if present it MUST be referred to via 'image'" ; + sh:path schema:image ; + sh:class schema:MediaObject, schema:ImageObject ; + sh:minCount 1 ; + sh:message "The Crate MAY contain a Main Workflow Diagram; if present it MUST be referred to via 'image'" ; + # sh:severity sh:Info ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Main Workflow subjectOf" ; + sh:description "The Crate MAY contain a Main Workflow CWL Description; if present it MUST be referred to via 'subjectOf'" ; + sh:path schema:subjectOf ; + sh:node workflow-ro-crate:CWLDescriptionProperties ; + sh:minCount 1 ; + sh:message "The Crate MAY contain a Main Workflow CWL Description; if present it MUST be referred to via 'subjectOf'" ; + # sh:severity sh:Info ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Main Workflow step" ; + sh:description "The Main Workflow MAY have a step property" ; + sh:path schema:step ; + sh:minCount 1 ; + sh:message "The Main Workflow MAY have a step property" ; + # sh:severity sh:Info ; + ] . + +workflow-ro-crate:CWLDescriptionProperties a sh:NodeShape ; + sh:name "CWL Description properties" ; + sh:description "Main Workflow CWL Description properties" ; + sh:property [ + a sh:PropertyShape ; + sh:name "CWL Description type" ; + sh:description "The CWL Description type must be File, SoftwareSourceCode, HowTo" ; + sh:path rdf:type ; + sh:hasValue schema:MediaObject, schema:SoftwareSourceCode, schema:HowTo ; + sh:message "The CWL Description type must be File, SoftwareSourceCode, HowTo" ; + # sh:severity sh:Info ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "CWL Description language" ; + sh:description "The CWL Description SHOULD have a language of https://w3id.org/workflowhub/workflow-ro-crate#cwl" ; + sh:path schema:programmingLanguage ; + sh:hasValue wroc:cwl ; + sh:class schema:ComputerLanguage ; + sh:message "The CWL Description SHOULD have a language of https://w3id.org/workflowhub/workflow-ro-crate#cwl" ; + # sh:severity sh:Info ; + ] . diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/may/1_main_workflow.py b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/1_main_workflow.py new file mode 100644 index 000000000..6766d188f --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/1_main_workflow.py @@ -0,0 +1,83 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) + +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging + +# set up logging +logger = logging.getLogger(__name__) + + +@requirement(name="Workflow-related files existence") +class WorkflowFilesExistence(PyFunctionCheck): + """Checks for workflow-related crate files existence.""" + + @check(name="Workflow diagram existence") + def check_workflow_diagram(self, context: ValidationContext) -> CheckResultValue: + """Check if the crate contains the workflow diagram.""" + if context.settings.metadata_only: + logger.debug("Skipping file descriptor existence check in metadata-only mode") + context.record_skip(self, "metadata-only mode", "configured") + return CheckResult.SKIPPED + + try: + main_workflow = context.ro_crate.metadata.get_main_workflow() + image = main_workflow.get_property("image") + diagram_relpath = image.id if image else None + if not diagram_relpath: + context.result.add_issue("main workflow does not have an 'image' property", self) + return False + if not image.is_available(): + context.result.add_issue(f"Workflow diagram '{image.id}' not found in crate", self) + return False + return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping workflow diagram check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except (AttributeError, OSError, TypeError, ValueError) as e: + context.result.add_issue(f"Unable to check the workflow diagram: {e}", self) + if logger.isEnabledFor(logging.DEBUG): + logger.exception("Unexpected error checking main workflow image existence") + return False + + @check(name="Workflow description existence") + def check_workflow_description(self, context: ValidationContext) -> CheckResultValue: + """Check if the crate contains the workflow CWL description.""" + try: + main_workflow = context.ro_crate.metadata.get_main_workflow() + main_workflow_subject = main_workflow.get_property("subjectOf") + description_relpath = main_workflow_subject.id if main_workflow_subject else None + if not description_relpath: + context.result.add_issue("main workflow does not have a 'subjectOf' property", self) + return False + if not context.settings.metadata_only and not main_workflow_subject.is_available(): + context.result.add_issue( + f"Workflow CWL description {main_workflow_subject.id} not found in crate", self + ) + return False + return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping workflow description check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except (AttributeError, OSError, TypeError, ValueError) as e: + context.result.add_issue(f"Unable to check the workflow description: {e}", self) + if logger.isEnabledFor(logging.DEBUG): + logger.exception("Unexpected error checking workflow description existence") + return False diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/may/2_wrroc_crate.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/2_wrroc_crate.ttl new file mode 100644 index 000000000..3bd2e2476 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/2_wrroc_crate.ttl @@ -0,0 +1,49 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix workflow-ro-crate: . +@prefix rdf: . +@prefix schema: . +@prefix bioschemas: . +@prefix sh: . + +workflow-ro-crate:FindTestDir a sh:NodeShape ; + sh:name "test directory" ; + sh:description "Dataset data entity to hold tests" ; + sh:targetNode ro:test\/ ; + sh:property [ + a sh:PropertyShape ; + sh:name "test/ dir type" ; + sh:description "The test/ dir should be a Dataset" ; + sh:path rdf:type ; + sh:hasValue schema:Dataset ; + sh:minCount 1 ; + sh:message "The test/ dir should be a Dataset" ; + ] . + +workflow-ro-crate:FindExamplesDir a sh:NodeShape ; + sh:name "examples directory" ; + sh:description "Dataset data entity to hold examples" ; + sh:targetNode ro:examples\/ ; + sh:property [ + a sh:PropertyShape ; + sh:name "examples/ dir type" ; + sh:description "The examples/ dir should be a Dataset" ; + sh:path rdf:type ; + sh:hasValue schema:Dataset ; + sh:minCount 1 ; + sh:message "The examples/ dir should be a Dataset" ; + ] . diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/may/3_howtostep.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/3_howtostep.ttl new file mode 100644 index 000000000..e48a07c35 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/3_howtostep.ttl @@ -0,0 +1,30 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix workflow-ro-crate: . +@prefix schema: . +@prefix sh: . + +workflow-ro-crate:WROCHowToStepOptional a sh:NodeShape ; + sh:name "WROC HowToStep MAY" ; + sh:description "Optional properties of a Workflow RO-Crate HowToStep" ; + sh:targetClass schema:HowToStep ; + sh:property [ + a sh:PropertyShape ; + sh:name "HowToStep position" ; + sh:description "A HowToStep may indicate its position in the execution order via position" ; + sh:path schema:position ; + sh:minCount 1 ; + sh:message "A HowToStep may indicate its position in the execution order via position" ; + ] . diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/may/4_computational_workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/4_computational_workflow.ttl new file mode 100644 index 000000000..01e930ebc --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/may/4_computational_workflow.ttl @@ -0,0 +1,39 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix schema: . +@prefix bioschemas: . +@prefix sh: . +@prefix workflow-ro-crate: . + +workflow-ro-crate:WROCComputationalWorkflowOptional a sh:NodeShape ; + sh:name "Workflow RO-Crate ComputationalWorkflow MAY" ; + sh:description "Optional properties of a Workflow RO-Crate ComputationalWorkflow" ; + sh:targetClass bioschemas:ComputationalWorkflow ; + sh:property [ + a sh:PropertyShape ; + sh:name "ComputationalWorkflow input" ; + sh:description "A ComputationalWorkflow MAY have an input" ; + sh:path bioschemas:input ; + sh:minCount 1 ; + sh:message "A ComputationalWorkflow MAY have an input" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "ComputationalWorkflow output" ; + sh:description "A ComputationalWorkflow MAY have an output" ; + sh:path bioschemas:output ; + sh:minCount 1 ; + sh:message "A ComputationalWorkflow MAY have an output" ; + ] . diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/must/0_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/must/0_main-workflow.ttl new file mode 100644 index 000000000..103c4eb17 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/must/0_main-workflow.ttl @@ -0,0 +1,125 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix workflow-ro-crate: . +@prefix bioschemas: . +@prefix rdf: . +@prefix schema: . +@prefix sh: . +@prefix validator: . + +workflow-ro-crate:MainEntityPropertyMustExist a sh:NodeShape ; + sh:name "Main Workflow entity existence" ; + sh:description "The Main Workflow must be specified through a `mainEntity` property in the root data entity" ; + sh:targetClass ro-crate:RootDataEntity ; + sh:property [ + a sh:PropertyShape ; + sh:path schema:mainEntity ; + sh:minCount 1 ; + sh:description "Check if the Main Workflow is specified through a `mainEntity` property in the root data entity" ; + sh:message "The Main Workflow must be specified through a `mainEntity` property in the root data entity" ; + ] . + +workflow-ro-crate:FindMainWorkflow a sh:NodeShape, validator:HiddenShape ; + sh:name "Identify Main Workflow" ; + sh:description "Identify the Main Workflow" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?root schema:mainEntity ?this . + ?root a schema:Dataset . + ?metadatafile schema:about ?root . + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) + } + """ + ] ; + sh:rule [ + a sh:TripleRule ; + sh:subject sh:this ; + sh:predicate rdf:type ; + sh:object workflow-ro-crate:MainWorkflow ; + ] . + +workflow-ro-crate:MainWorkflowRequiredProperties a sh:NodeShape ; + sh:name "Main Workflow definition" ; + sh:description """Main Workflow properties defined as MUST"""; + sh:targetClass workflow-ro-crate:MainWorkflow ; + sh:property [ + a sh:PropertyShape ; + sh:name "Main Workflow type" ; + sh:description "The Main Workflow types must include File, SoftwareSourceCode and ComputationalWorkflow" ; + sh:path rdf:type ; + sh:hasValue schema:MediaObject , + schema:SoftwareSourceCode , + bioschemas:ComputationalWorkflow ; + sh:minCount 1 ; + sh:message "The Main Workflow types must include File, SoftwareSourceCode and ComputationalWorkflow" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Main Workflow language" ; + sh:description "The Main Workflow must refer to its language via programmingLanguage" ; + sh:path schema:programmingLanguage ; + sh:class schema:ComputerLanguage ; + sh:minCount 1 ; + sh:message "The Main Workflow must refer to its language via programmingLanguage" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Main Workflow step" ; + sh:description "Main Workflow step MUST point to HowToStep entities" ; + sh:path schema:step ; + sh:class schema:HowToStep ; + sh:message "Main Workflow step MUST point to HowToStep entities" ; + ] . + + +workflow-ro-crate:MainWorkflowWithSteps a sh:NodeShape ; + sh:name "Main Workflow with steps MUST" ; + sh:description "Required properties of a Main Workflow that links to steps" ; + sh:target [ + a sh:SPARQLTarget ; + sh:prefixes ro-crate:sparqlPrefixes ; + sh:select """ + SELECT ?this + WHERE { + ?root schema:mainEntity ?this . + ?root a schema:Dataset . + ?metadatafile schema:about ?root . + FILTER( + STRENDS(str(?metadatafile), "ro-crate-metadata.json") || + STRENDS(str(?metadatafile), "ro-crate-metadata.jsonld") + ) . + ?this schema:step ?step . + ?step a schema:HowToStep + } + """ + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "Main Workflow with steps type" ; + sh:description "A Main Workflow that links to steps MUST have the HowTo type" ; + sh:path rdf:type ; + sh:hasValue schema:HowTo; + sh:minCount 1 ; + sh:message "A Main Workflow that links to steps MUST have the HowTo type" ; + ] . diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/must/0_main_workflow.py b/rocrate_validator/profiles/workflow-ro-crate/1.1/must/0_main_workflow.py new file mode 100644 index 000000000..86501fbb4 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/must/0_main_workflow.py @@ -0,0 +1,57 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=invalid-name # profile filename uses digit prefix (load-order convention) + +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import CheckResult, CheckResultValue, ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement +from rocrate_validator.utils import log as logging + +# set up logging +logger = logging.getLogger(__name__) + + +@requirement(name="Main Workflow file existence") +class MainWorkflowFileExistence(PyFunctionCheck): + """Checks for main workflow file existence.""" + + @check(name="Main Workflow file must exist") + def check_workflow(self, context: ValidationContext) -> CheckResultValue: + """Check if the crate contains the main workflow file.""" + try: + main_workflow = context.ro_crate.metadata.get_main_workflow() + if not main_workflow: + context.result.add_issue("main workflow does not exist in metadata file", self) + return False + if not context.settings.metadata_only and not main_workflow.is_available(): + context.result.add_issue(f"Main Workflow {main_workflow.id} not found in crate", self) + return False + return True + except ROCrateMetadataNotFoundError: + logger.debug("Skipping main workflow check: metadata descriptor is not available") + context.record_skip(self, "metadata descriptor is not available", "exception") + return CheckResult.SKIPPED + except (TypeError, ValueError) as error: + if str(error) == "no main workflow in metadata file descriptor": + message = ( + "Unable to check the existence of the main workflow file " + "because the metadata file descriptor doesn't contain a `mainEntity`" + ) + else: + message = f"Unable to check the existence of the main workflow file: {error}" + context.result.add_issue(message, self) + if logger.isEnabledFor(logging.DEBUG): + logger.exception("Unable to check main workflow file existence") + return False diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/must/1_wroc_root_data_entity.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/must/1_wroc_root_data_entity.ttl new file mode 100644 index 000000000..5615d5c10 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/must/1_wroc_root_data_entity.ttl @@ -0,0 +1,39 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix workflow-ro-crate: . +@prefix schema: . +@prefix bioschemas: . +@prefix sh: . +@prefix xsd: . +@prefix validator: . + +workflow-ro-crate:WROCRootDataEntityRequiredProperties a sh:NodeShape ; + sh:name "WROC Root Data Entity Required Properties" ; + sh:description """Root Data Entity properties defined as MUST""" ; + sh:targetClass ro-crate:RootDataEntity ; + sh:property [ + a sh:PropertyShape ; + sh:name "Crate license" ; + sh:description "The Crate must specify a license" ; + sh:path schema:license ; + sh:or ( + [ sh:nodeKind sh:Literal; sh:datatype xsd:string ; ] + [ sh:nodeKind sh:IRI ; ] + ) ; + sh:minCount 1 ; + sh:message "The Crate (Root Data Entity) must specify a license, which should be a URL but can also be a string" ; + ] . diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/must/2_computational_workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/must/2_computational_workflow.ttl new file mode 100644 index 000000000..eed4a6448 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/must/2_computational_workflow.ttl @@ -0,0 +1,32 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix bioschemas: . +@prefix sh: . +@prefix workflow-ro-crate: . + +workflow-ro-crate:WROCComputationalWorkflowRequired a sh:NodeShape ; + sh:name "Workflow RO-Crate ComputationalWorkflow MUST" ; + sh:description "Required properties of the Workflow RO-Crate ComputationalWorkflow" ; + sh:targetClass bioschemas:ComputationalWorkflow ; + sh:property [ + a sh:PropertyShape ; + sh:name "ComputationalWorkflow input and output" ; + sh:description "ComputationalWorkflow input and output MUST point to FormalParameter entities" ; + sh:path [ + sh:alternativePath (bioschemas:input bioschemas:output) ; + ] ; + sh:class bioschemas:FormalParameter ; + sh:message "ComputationalWorkflow input and output MUST point to FormalParameter entities" ; + ] . diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/profile.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/profile.ttl new file mode 100644 index 000000000..5626c8357 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/profile.ttl @@ -0,0 +1,83 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix dct: . +@prefix prof: . +@prefix role: . +@prefix rdfs: . + + + + + a prof:Profile ; + + # the Profile's label + rdfs:label "Workflow RO-Crate Metadata Specification 1.1" ; + + # regular metadata, a basic description of the Profile + rdfs:comment """Workflow RO-Crate Metadata Specification."""@en ; + + # URI of the publisher of the Workflow RO-Crate Metadata Specification + dct:publisher ; + + # This profile is an extension of the RO-Crate Metadata Specification 1.3 profile + prof:isProfileOf ; + + # Explicitly state that this profile is a transitive profile of the RO-Crate Metadata Specification 1.3 profile + prof:isTransitiveProfileOf + , + ; + + # this profile has a JSON-LD context resource + prof:hasResource [ + a prof:ResourceDescriptor ; + + # it's in JSON-LD format + dct:format ; + + # it conforms to JSON-LD, here referred to by its namespace URI as a Profile + dct:conformsTo ; + + # this profile resource plays the role of "Vocabulary" + # described in this ontology's accompanying Roles vocabulary + prof:hasRole role:Vocabulary ; + + # this profile resource's actual file + prof:hasArtifact ; + ] ; + + # this profile has a human-readable documentation resource + prof:hasResource [ + a prof:ResourceDescriptor ; + + # it's in HTML format + dct:format ; + + # it conforms to HTML, here referred to by its namespace URI as a Profile + dct:conformsTo ; + + # this profile resource plays the role of "Specification" + # described in this ontology's accompanying Roles vocabulary + prof:hasRole role:Specification ; + + # this profile resource's actual file + prof:hasArtifact ; + + # this profile is inherited from the RO-Crate Metadata Specification 1.3 + prof:isInheritedFrom ; + ] ; + + # a short code to refer to the Profile with when a URI can't be used + prof:hasToken "workflow-ro-crate" ; +. diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/should/1_wroc_crate.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/should/1_wroc_crate.ttl new file mode 100644 index 000000000..2cc6ab489 --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/should/1_wroc_crate.ttl @@ -0,0 +1,64 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix workflow-ro-crate: . +@prefix dct: . +@prefix bioschemas: . +@prefix schema: . +@prefix sh: . + + +workflow-ro-crate:WROCRootDataEntityMetadata a sh:NodeShape ; + sh:name "Root Data Entity Metadata" ; + sh:description "Properties of the Root Data Entity" ; + sh:targetClass ro-crate:RootDataEntity ; + sh:property [ + a sh:PropertyShape ; + sh:name "Root Data Entity conformsTo" ; + sh:description "The Root Data Entity conformsTo SHOULD be an array that contains at least https://w3id.org/workflowhub/workflow-ro-crate/1.1" ; + sh:path dct:conformsTo ; + sh:class schema:CreativeWork; + # At least one value of conformsTo must match the URI + sh:qualifiedValueShape [ + sh:pattern "^https://w3id.org/workflowhub/workflow-ro-crate/1\\.1$" ; + ] ; + sh:qualifiedMinCount 1 ; + sh:minCount 1 ; + sh:message "The Root Data Entity conformsTo SHOULD be an array that contains at least https://w3id.org/workflowhub/workflow-ro-crate/1.1" ; + ] . + +workflow-ro-crate:FindReadme a sh:NodeShape ; + sh:name "README.md properties" ; + sh:description "README file for the Workflow RO-Crate" ; + sh:targetNode ro:README.md ; + sh:property [ + a sh:PropertyShape ; + sh:name "README.md about" ; + sh:description "The README.md SHOULD be about the crate" ; + sh:path schema:about ; + sh:class ro-crate:RootDataEntity ; + sh:minCount 1 ; + sh:message "The README.md SHOULD be about the crate" ; + ] ; + sh:property [ + a sh:PropertyShape ; + sh:name "README.md encodingFormat" ; + sh:description "The README.md SHOULD have text/markdown as its encodingFormat" ; + sh:path schema:encodingFormat ; + sh:hasValue "text/markdown" ; + sh:minCount 1 ; + sh:message "The README.md SHOULD have text/markdown as its encodingFormat" ; + ] . diff --git a/rocrate_validator/profiles/workflow-ro-crate/1.1/should/2_main-workflow.ttl b/rocrate_validator/profiles/workflow-ro-crate/1.1/should/2_main-workflow.ttl new file mode 100644 index 000000000..b0b74bb1f --- /dev/null +++ b/rocrate_validator/profiles/workflow-ro-crate/1.1/should/2_main-workflow.ttl @@ -0,0 +1,35 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix ro-crate: . +@prefix workflow-ro-crate: . +@prefix dct: . +@prefix sh: . +@prefix bioschemas: . + +workflow-ro-crate:MainWorkflowRecommendedProperties a sh:NodeShape ; + sh:name "Main Workflow recommended properties" ; + sh:description """Main Workflow properties defined as SHOULD"""; + sh:targetClass workflow-ro-crate:MainWorkflow ; + sh:property [ + a sh:PropertyShape ; + sh:name "Main Workflow Bioschemas compliance" ; + sh:description "The Main Workflow SHOULD comply with Bioschemas ComputationalWorkflow profile version 1.0 or later" ; + sh:path dct:conformsTo ; + sh:pattern "https://bioschemas.org/profiles/ComputationalWorkflow/[1-9].*" ; + sh:minCount 1 ; + sh:message "The Main Workflow SHOULD comply with Bioschemas ComputationalWorkflow profile version 1.0 or later" ; + sh:severity sh:Warning ; + ] . diff --git a/rocrate_validator/profiles/workflow-run-crate/must/4_create_action.ttl b/rocrate_validator/profiles/workflow-run-crate/must/4_create_action.ttl new file mode 100644 index 000000000..7d0d22ad1 --- /dev/null +++ b/rocrate_validator/profiles/workflow-run-crate/must/4_create_action.ttl @@ -0,0 +1,42 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +@prefix ro: <./> . +@prefix dct: . +@prefix ro-crate: . +@prefix schema: . +@prefix bioschemas: . +@prefix bioschemas-cw: . +@prefix sh: . +@prefix workflow-run-crate: . + + +workflow-run-crate:CreateActionWithMainEntityAsInstrumentRequired a sh:NodeShape ; + sh:targetClass ro-crate:RootDataEntity ; + sh:property [ + sh:name "Main entity execution" ; + sh:path ( + schema:mainEntity + [ sh:inversePath schema:instrument ] + ) ; + sh:qualifiedValueShape [ + sh:or ( + [ sh:class schema:CreateAction ] + [ sh:class schema:ActivateAction ] + [ sh:class schema:UpdateAction ] + ) ; + ] ; + sh:qualifiedMinCount 1 ; + sh:message "At least one CreateAction, ActivateAction or UpdateAction MUST reference the root data entity's mainEntity as its instrument." ; + ] . diff --git a/rocrate_validator/requirements/python/__init__.py b/rocrate_validator/requirements/python/__init__.py index c6bfa6f3e..96b440be7 100644 --- a/rocrate_validator/requirements/python/__init__.py +++ b/rocrate_validator/requirements/python/__init__.py @@ -16,10 +16,13 @@ import re from collections.abc import Callable from pathlib import Path -from typing import Any, cast +from types import UnionType +from typing import Any, Union, cast, get_args, get_origin, get_type_hints from rocrate_validator.constants import EXPECTED_CHECK_PARAM_COUNT from rocrate_validator.models import ( + CheckResult, + CheckResultValue, LevelCollection, Profile, Requirement, @@ -46,15 +49,26 @@ def __init__( self, requirement: Requirement, # pylint: disable=redefined-outer-name name: str, - check_function: Callable[[RequirementCheck, ValidationContext], bool], + check_function: Callable[[RequirementCheck, ValidationContext], CheckResultValue], + *, description: str | None = None, level: RequirementLevel | None = LevelCollection.REQUIRED, deactivated: bool = False, + depends_on: tuple[str, ...] | None = None, + extra_check: bool = False, ): """ check_function: a function that accepts an instance of PyFunctionCheck and a ValidationContext. """ - super().__init__(requirement, name, description=description, level=level, deactivated=deactivated) + super().__init__( + requirement, + name, + description=description, + level=level, + deactivated=deactivated, + depends_on=tuple(getattr(check_function, "depends_on", depends_on or ())), + extra_check=extra_check, + ) sig = inspect.signature(check_function) if len(sig.parameters) != EXPECTED_CHECK_PARAM_COUNT: @@ -62,17 +76,31 @@ def __init__( "Invalid PyFunctionCheck function. Checks are expected to accept " f"arguments [RequirementCheck, ValidationContext] but this has signature {sig}" ) - if sig.return_annotation not in (bool, inspect.Signature.empty): + try: + return_annotation = get_type_hints(check_function).get("return", sig.return_annotation) + except (NameError, TypeError): + # Keep the original annotation for dynamically loaded functions whose + # forward references cannot be resolved in their defining module. + return_annotation = sig.return_annotation + allowed_return_types = {bool, CheckResult, type(None)} + return_origin = get_origin(return_annotation) + valid_return_annotation = ( + return_annotation is inspect.Signature.empty + or return_annotation in allowed_return_types + or (return_origin in (Union, UnionType) and set(get_args(return_annotation)).issubset(allowed_return_types)) + ) + if not valid_return_annotation: raise RuntimeError( "Invalid PyFunctionCheck function. Checks are expected to " - f"return bool but this only returns {sig.return_annotation}" + f"return bool, CheckResult, or None but this only returns {return_annotation}" ) self._check_function = check_function - def execute_check(self, context: ValidationContext) -> bool: + def execute_check(self, context: ValidationContext) -> CheckResultValue: if ( self.requirement.profile.identifier != context.profile_identifier + and not context.is_rule_overlay_source(self.requirement.profile) and context.settings.disable_inherited_profiles_issue_reporting ): logger.debug( @@ -80,7 +108,7 @@ def execute_check(self, context: ValidationContext) -> bool: self.requirement.identifier, self.requirement.profile.identifier, ) - return True + return CheckResult.SKIPPED return self._check_function(self, context) def get_source_snippet(self) -> SourceSnippet | None: @@ -155,6 +183,7 @@ def __init_checks__(self): severity = self.severity_from_path or Severity.REQUIRED logger.debug("Severity log: %r", severity) deactivated = bool(getattr(member, "deactivated", False)) + extra_check = bool(getattr(member, "extra_check", False)) # pylint: disable-next=redefined-outer-name # local 'check' mirrors the decorator name check = self.requirement_check_class( self, @@ -163,6 +192,7 @@ def __init_checks__(self): description=check_description, level=LevelCollection.get(severity.name) if severity else None, deactivated=deactivated, + extra_check=extra_check, ) self._checks.append(check) logger.debug("Added check: %s %r", check_name, check) @@ -204,7 +234,13 @@ def decorator(cls): return decorator -def check(name: str | None = None, severity: Severity | None = None, deactivated: bool = False): +def check( + name: str | None = None, + severity: Severity | None = None, + deactivated: bool = False, + depends_on: tuple[str, ...] | None = None, + extra_check: bool = False, +): """ A decorator to mark a function as a check. @@ -213,7 +249,7 @@ def check(name: str | None = None, severity: Severity | None = None, deactivated - a :py:class:`rocrate_validator.models.RequirementCheck` instance - a :py:class:`rocrate_validator.models.ValidationContext` instance - The function should return a boolean value. + The function should return a boolean value, ``CheckResult``, or ``None``. The decorator can be used to set the name of the check and the severity level. @@ -228,7 +264,10 @@ def check(name: str | None = None, severity: Severity | None = None, deactivated a check with the same name as one in a parent profile and set this flag to disable the inherited check. :type deactivated: bool - + :param depends_on: names of checks that must run before this check. + :type depends_on: Optional[Tuple[str, ...]] + :param extra_check: mark this as an advisory check that does not determine conformance. + :type extra_check: bool :return: the decorated function :rtype: Callable """ @@ -241,15 +280,30 @@ def decorator(func): f"Invalid check {check_name}. Checks are expected to " f"accept two arguments but this only takes {len(sig.parameters)}" ) - if sig.return_annotation not in (bool, inspect.Signature.empty): + if depends_on is not None and ( + not isinstance(depends_on, tuple) or not all(isinstance(dependency, str) for dependency in depends_on) + ): + raise TypeError("Check dependencies must be provided as a tuple of strings") + try: + return_annotation = get_type_hints(func).get("return", sig.return_annotation) + except (NameError, TypeError): + return_annotation = sig.return_annotation + valid_return_annotation = return_annotation in (bool, CheckResult, type(None)) + if get_origin(return_annotation) in (UnionType, Union): + valid_return_annotation = all( + annotation in (bool, CheckResult, type(None)) for annotation in get_args(return_annotation) + ) + if return_annotation is not inspect.Signature.empty and not valid_return_annotation: raise RuntimeError( f"Invalid check {check_name}. Checks are expected to " - f"return bool but this only returns {sig.return_annotation}" + f"return bool, CheckResult, or None but this returns {sig.return_annotation}" ) func.check = True func.name = check_name func.severity = severity func.deactivated = deactivated + func.depends_on = depends_on or () + func.extra_check = extra_check return func return decorator diff --git a/rocrate_validator/requirements/shacl/checks.py b/rocrate_validator/requirements/shacl/checks.py index de5475a6c..fda7868a3 100644 --- a/rocrate_validator/requirements/shacl/checks.py +++ b/rocrate_validator/requirements/shacl/checks.py @@ -28,7 +28,7 @@ RequirementCheckValidationEvent, RequirementLevel, Severity, - SkipRequirementCheck, + SkipCategory, SourceSnippet, ValidationContext, ) @@ -38,7 +38,6 @@ SHACLValidationAlreadyProcessed, SHACLValidationContext, SHACLValidationContextManager, - SHACLValidationSkip, SHACLValidator, SHACLViolation, ) @@ -62,6 +61,7 @@ def __init__( self, requirement: Requirement, shape: Shape, + *, name: str | None = None, root: bool = False, hidden: bool | None = None, @@ -128,11 +128,7 @@ def deactivated(self) -> bool: owning_profile = self.requirement.profile for profile in Profile.get_descendants(owning_profile): - try: - registry = ShapesRegistry.get_instance(profile) - except Exception as e: - logger.debug("Unable to get shapes registry for profile %s: %s", profile.identifier, e) - continue + registry = ShapesRegistry.get_instance(profile) if registry.is_node_deactivated(shape.node): return True return False @@ -246,7 +242,7 @@ def execute_check(self, context: ValidationContext): self.identifier, ) result = self.__do_execute_check__(ctx) - ctx.current_validation_result = self.identifier not in result + ctx.current_validation_result = self not in result logger.debug( "SHACL Validation of requirement check %s (profile: %s) finished with result %s", self.requirement.profile.identifier, @@ -263,21 +259,12 @@ def execute_check(self, context: ValidationContext): # The check belongs to a profile which has already been processed # so we can skip the validation and return the specific result for the check return self.identifier not in [i.check.identifier for i in context.result.get_issues()] - except SHACLValidationSkip as e: - logger.debug( - "SHACL Validation of profile %s requirement %s skipped", - self.requirement.profile.identifier, - self.identifier, - ) - # The validation is postponed to the more specific profiles - # so the check is not considered as failed. - raise SkipRequirementCheck(self, str(e)) from e except ROCrateMetadataNotFoundError as e: logger.debug( "Unable to perform metadata validation due to missing metadata file: %s", e, ) - return False + return True def __do_execute_check__(self, shacl_context: SHACLValidationContext): # get the shapes registry @@ -310,7 +297,7 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): # Begin the timer start_time = timer() - shapes_graph = shapes_registry.shapes_graph + shapes_graph = shacl_context.shapes_graph end_time = timer() logger.debug(f"Execution time for getting shapes: {end_time - start_time} seconds") @@ -343,10 +330,8 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext): failed_requirements_checks, failed_requirements_checks_violations = self.__collect_failed_checks__( shacl_context, shacl_result, shapes_registry, shapes_graph ) - failed_requirement_checks_notified = self.__process_failed_checks__( - shacl_context, failed_requirements_checks, failed_requirements_checks_violations - ) - self.__notify_skipped_checks__(shacl_context, failed_requirement_checks_notified) + self.__process_failed_checks__(shacl_context, failed_requirements_checks, failed_requirements_checks_violations) + self.__notify_deferred_checks__(shacl_context, failed_requirements_checks) logger.debug("Remaining skipped checks: %r", len(shacl_context.result.skipped_checks)) for skipped_check in shacl_context.result.skipped_checks: @@ -422,6 +407,7 @@ def __process_failed_checks__( # and the disable_inherited_profiles_reporting is enabled, skip it if ( requirementCheck.requirement.profile != shacl_context.current_validation_profile + and not shacl_context.is_rule_overlay_source(requirementCheck.requirement.profile) and shacl_context.settings.disable_inherited_profiles_issue_reporting ): continue @@ -431,24 +417,17 @@ def __process_failed_checks__( failed_requirements_checks_violations[requirementCheck.identifier], ) - # If the fail fast mode is disabled, notify all the validation issues - # related to profiles other than the current one. - # They are issues which have not been notified yet because skipped during - # the validation of their corresponding profile because SHACL checks are executed - # all together and not profile by profile + # Notify every batched outcome, including target-local checks: + # traversal will reuse their recorded results without executing them. if requirementCheck.identifier not in failed_requirement_checks_notified: shacl_context.result._add_executed_check(requirementCheck, False) - if requirementCheck.requirement.profile != shacl_context.current_validation_profile: - failed_requirement_checks_notified.append(requirementCheck.identifier) - shacl_context.validator.notify( - RequirementCheckValidationEvent( - EventType.REQUIREMENT_CHECK_VALIDATION_END, requirementCheck, validation_result=False - ) - ) - logger.debug( - "Added failed check to the context: %s", - requirementCheck.identifier, + failed_requirement_checks_notified.append(requirementCheck.identifier) + shacl_context.validator.notify( + RequirementCheckValidationEvent( + EventType.REQUIREMENT_CHECK_VALIDATION_END, requirementCheck, validation_result=False ) + ) + logger.debug("Added failed check to the context: %s", requirementCheck.identifier) # if the fail fast mode is enabled, stop the validation after the first failed check if shacl_context.fail_fast: @@ -492,31 +471,34 @@ def __register_check_violations__(self, shacl_context, requirementCheck, violati if shacl_context.fail_fast: break - def __notify_skipped_checks__(self, shacl_context, failed_requirement_checks_notified): - for skipped_check in list(shacl_context.result.skipped_checks): - logger.debug("Processing skipped check: %s", skipped_check.identifier) - if not isinstance(skipped_check, SHACLCheck): - logger.debug("Skipped check is not a SHACLCheck: %s", skipped_check.identifier) + def __notify_deferred_checks__(self, shacl_context, failed_checks): + """Complete pending checks without changing genuine skip outcomes.""" + for check in sorted(shacl_context.deferred_checks): + if shacl_context.result.get_check_result(check) is not None: continue - if skipped_check.identifier not in failed_requirement_checks_notified: - failed_requirement_checks_notified.append(skipped_check.identifier) - shacl_context.result._add_executed_check(skipped_check, True) - if ( - skipped_check.requirement.profile != shacl_context.target_profile - and shacl_context.settings.disable_inherited_profiles_issue_reporting - ): - continue - shacl_context.validator.notify( - RequirementCheckValidationEvent( - EventType.REQUIREMENT_CHECK_VALIDATION_END, - skipped_check, - validation_result=True, - ) + if shacl_context.fail_fast and failed_checks: + Requirement.__record_skipped_check__( + check, + shacl_context.base_context, + "SHACL validation stopped before this check's outcome was determined", + SkipCategory.NOT_REACHED, ) - logger.debug( - "Added skipped check to the context: %s", - skipped_check.identifier, + continue + shacl_context.result._add_executed_check(check, True) + if ( + check.requirement.profile != shacl_context.target_profile + and not shacl_context.is_rule_overlay_source(check.requirement.profile) + and shacl_context.settings.disable_inherited_profiles_issue_reporting + ): + continue + shacl_context.validator.notify( + RequirementCheckValidationEvent( + EventType.REQUIREMENT_CHECK_VALIDATION_END, + check, + validation_result=True, ) + ) + shacl_context.deferred_checks.clear() @classmethod def get_instance(cls, shape: Shape) -> Optional["SHACLCheck"]: diff --git a/rocrate_validator/requirements/shacl/errors.py b/rocrate_validator/requirements/shacl/errors.py index ffccca793..fa1b14e8d 100644 --- a/rocrate_validator/requirements/shacl/errors.py +++ b/rocrate_validator/requirements/shacl/errors.py @@ -13,11 +13,17 @@ # limitations under the License. -from rocrate_validator.errors import ValidationError -from rocrate_validator.requirements.shacl.validator import SHACLValidationResult +from __future__ import annotations +from typing import TYPE_CHECKING -class SHACLValidationError(ValidationError): +from rocrate_validator.errors import ValidationExecutionError + +if TYPE_CHECKING: + from rocrate_validator.requirements.shacl.validator import SHACLValidationResult + + +class SHACLValidationError(ValidationExecutionError): def __init__( self, result: SHACLValidationResult | None = None, diff --git a/rocrate_validator/requirements/shacl/models.py b/rocrate_validator/requirements/shacl/models.py index c7b439c89..6a0e336a7 100644 --- a/rocrate_validator/requirements/shacl/models.py +++ b/rocrate_validator/requirements/shacl/models.py @@ -301,6 +301,7 @@ class ShapesRegistry: def __init__(self): self._shapes: dict[str, Shape] = {} self._shapes_graph: Graph = Graph() + self._relative_iris: set[Node] = set() def add_shape(self, shape: Shape): assert isinstance(shape, Shape), "Invalid shape" @@ -338,6 +339,11 @@ def shapes_graph(self) -> Graph: g += self._shapes_graph return g + @property + def relative_iris(self) -> set[Node]: + """IRIs in loaded Turtle shapes that were written as relative references.""" + return self._relative_iris.copy() + def is_node_deactivated(self, node: Node) -> bool: """Return True if the underlying shapes graph asserts ` sh:deactivated true`. Avoids the copy made by `shapes_graph` @@ -355,6 +361,7 @@ def load_shapes(self, shapes_path: str | Path, publicID: str | None = None) -> l logger.debug(f"Loading shapes from: {shapes_path}") # load shapes (nodes and properties) from the shapes graph shapes_list: ShapesList = ShapesList.load_from_file(str(shapes_path), publicID) + self._relative_iris.update(shapes_list.relative_iris) logger.debug(f"Shapes List: {shapes_list}") # append the partial shapes graph to the global shapes graph @@ -429,6 +436,7 @@ def __repr__(self): def clear(self): self._shapes.clear() self._shapes_graph = Graph() + self._relative_iris.clear() @classmethod def get_instance(cls, ctx: object): diff --git a/rocrate_validator/requirements/shacl/requirements.py b/rocrate_validator/requirements/shacl/requirements.py index f2ff5ce10..d46b48d27 100644 --- a/rocrate_validator/requirements/shacl/requirements.py +++ b/rocrate_validator/requirements/shacl/requirements.py @@ -18,12 +18,14 @@ from rdflib import RDF from rocrate_validator.constants import VALIDATOR_NS +from rocrate_validator.errors import ROCrateMetadataNotFoundError, ValidationExecutionError from rocrate_validator.models import ( Profile, Requirement, RequirementCheck, RequirementLevel, RequirementLoader, + SkipCategory, ValidationContext, ) from rocrate_validator.requirements.shacl.checks import SHACLCheck @@ -101,22 +103,29 @@ def finalize(cls, context: ValidationContext) -> None: logger.debug("Starting %s requirement finalization for context %s", cls.__name__, context) + from rocrate_validator.requirements.shacl.validator import SHACLValidationContext # noqa: PLC0415 + + shacl_context = SHACLValidationContext.get_instance(context) # If the validation was aborted (e.g. the metadata is not valid JSON), the data # graph cannot be parsed: skip the forced SHACL run to avoid false positives. - if context.aborted: + if context.aborted or (context.fail_fast and context.result.has_issues()): + cls.__record_skipped_checks__( + shacl_context.deferred_checks, + context, + "Validation stopped before the deferred SHACL checks were executed", + SkipCategory.NOT_REACHED, + ) + shacl_context.deferred_checks.clear() logger.debug("Skipping forced SHACL run: validation aborted (%s)", context.abort_reason) return # extract profiles and target profile from context profiles = context.profiles - from rocrate_validator.requirements.shacl.validator import SHACLValidationContext # noqa: PLC0415 - target = next((p for p in profiles if p.identifier == context.settings.profile_identifier), None) if target is None: return - shacl_context = SHACLValidationContext.get_instance(context) # If pyshacl already ran for the target during the main loop there is # nothing to do. if shacl_context.get_validation_result(target) is not None: @@ -135,10 +144,20 @@ def finalize(cls, context: ValidationContext) -> None: # Make sure the target's shapes (if any) are in the merged registry # and switch the current profile so violations are attributed under # the target profile in the report. - shacl_context.__set_current_validation_profile__(target) - shacl_context._current_validation_profile = target try: + shacl_context.__set_current_validation_profile__(target) + shacl_context._current_validation_profile = target runner.__do_execute_check__(shacl_context) + except (FileNotFoundError, ROCrateMetadataNotFoundError) as e: + logger.debug( + "Forced SHACL run for zero-shape target profile %s skipped: metadata descriptor is not available (%s)", + target.identifier, + e, + ) + except ValidationExecutionError: + # A failed engine run is not a validation result and must reach API/CLI + # callers instead of being reduced to a warning. + raise except Exception as e: if context.maybe_warn_offline_cache_miss(e): logger.debug( @@ -147,11 +166,22 @@ def finalize(cls, context: ValidationContext) -> None: e, ) else: - logger.warning("Forced SHACL run for zero-shape target profile %s failed: %s", target.identifier, e) - if logger.isEnabledFor(logging.DEBUG): - logger.exception("Forced SHACL run for zero-shape target profile failed") + raise ValidationExecutionError( + message=( + f"Unexpected error while finalizing SHACL validation for profile " + f"'{target.identifier}': {type(e).__name__}: {e}" + ), + path=str(target.path), + ) from e finally: shacl_context.__unset_current_validation_profile__() + cls.__record_skipped_checks__( + shacl_context.deferred_checks, + context, + "Deferred SHACL validation could not be completed", + SkipCategory.EXCEPTION, + ) + shacl_context.deferred_checks.clear() # do finalization logic here (empty for now) logger.debug("Completed %s requirement finalization for context %s", cls.__name__, context) diff --git a/rocrate_validator/requirements/shacl/transformers/__init__.py b/rocrate_validator/requirements/shacl/transformers/__init__.py new file mode 100644 index 000000000..8eb441a0a --- /dev/null +++ b/rocrate_validator/requirements/shacl/transformers/__init__.py @@ -0,0 +1,29 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Core API for graph transformers used by SHACL validation.""" + +from rocrate_validator.requirements.shacl.transformers.base import GraphTransformer +from rocrate_validator.requirements.shacl.transformers.pipeline import prepare_data_graph +from rocrate_validator.requirements.shacl.transformers.registry import graph_transformer +from rocrate_validator.requirements.shacl.transformers.vocabulary import ( + REQUIRES_GRAPH_TRANSFORMER_PREDICATE, +) + +__all__ = [ + "REQUIRES_GRAPH_TRANSFORMER_PREDICATE", + "GraphTransformer", + "graph_transformer", + "prepare_data_graph", +] diff --git a/rocrate_validator/requirements/shacl/transformers/base.py b/rocrate_validator/requirements/shacl/transformers/base.py new file mode 100644 index 000000000..dd9f96e3b --- /dev/null +++ b/rocrate_validator/requirements/shacl/transformers/base.py @@ -0,0 +1,57 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import inspect +from abc import ABC, abstractmethod +from collections.abc import Callable +from typing import ClassVar + +from rdflib import Graph, URIRef + +TransformerFunction = Callable[[Graph], Graph] + + +class GraphTransformer(ABC): + """ + Base class for automatically registered graph transformers. + + Concrete subclasses are registered as soon as their module is imported. + Abstract intermediate classes remain unregistered. + """ + + identifier: ClassVar[URIRef | None] = None + order: ClassVar[int] = 100 + annotation_predicates: ClassVar[frozenset[URIRef]] = frozenset() + + def __init_subclass__(cls, **kwargs: object) -> None: + super().__init_subclass__(**kwargs) + if not inspect.isabstract(cls): + if cls.identifier is None: + raise TypeError(f"Graph transformer {cls.__module__}.{cls.__qualname__} has no identifier") + # Imported lazily to keep the abstract contract independent from + # the registry module which stores concrete implementations. + from rocrate_validator.requirements.shacl.transformers.registry import ( # noqa: PLC0415 + register_transformer, + ) + + register_transformer(cls, cls.identifier, cls.order, cls.annotation_predicates) + + @abstractmethod + def transform(self, data_graph: Graph) -> Graph: + """Transform and return the graph passed by the orchestration pipeline.""" + + +__all__ = ["GraphTransformer", "TransformerFunction"] diff --git a/rocrate_validator/requirements/shacl/transformers/discovery.py b/rocrate_validator/requirements/shacl/transformers/discovery.py new file mode 100644 index 000000000..948a9474d --- /dev/null +++ b/rocrate_validator/requirements/shacl/transformers/discovery.py @@ -0,0 +1,44 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Discovery of graph transformers bundled with the validator.""" + +from __future__ import annotations + +import importlib +import pkgutil +from threading import Lock + +_DISCOVERY_LOCK = Lock() +_TRANSFORMERS_LOADED = False + + +def load_transformers() -> None: + """Import every bundled transformer module exactly once. + + Importing triggers function decorators and ``GraphTransformer`` subclass + hooks. The lock prevents concurrent validations from observing a partially + populated registry during first use. + """ + global _TRANSFORMERS_LOADED # noqa: PLW0603 + if _TRANSFORMERS_LOADED: + return + with _DISCOVERY_LOCK: + if _TRANSFORMERS_LOADED: + return + package = importlib.import_module("rocrate_validator.transformers") + package_prefix = f"{package.__name__}." + for module in pkgutil.walk_packages(package.__path__, package_prefix): + importlib.import_module(module.name) + _TRANSFORMERS_LOADED = True diff --git a/rocrate_validator/requirements/shacl/transformers/pipeline.py b/rocrate_validator/requirements/shacl/transformers/pipeline.py new file mode 100644 index 000000000..1131f1b4a --- /dev/null +++ b/rocrate_validator/requirements/shacl/transformers/pipeline.py @@ -0,0 +1,91 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Execution pipeline for registered graph transformers.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from rdflib import Dataset, Graph + +from rocrate_validator.requirements.shacl.transformers.discovery import load_transformers +from rocrate_validator.requirements.shacl.transformers.registry import get_registered_transformers + +if TYPE_CHECKING: + from collections.abc import Collection + + from rdflib import URIRef + + +def _copy_data_graph(source: Graph) -> Graph: + """Copy a data graph without flattening RDFLib dataset contexts.""" + if isinstance(source, Dataset): + target: Graph = Dataset( + default_union=source.default_union, + default_graph_base=source.default_graph.base, + ) + else: + target = Graph(identifier=source.identifier, base=source.base) + + for prefix, namespace in source.namespaces(): + target.bind(prefix, namespace, replace=True) + + if isinstance(source, Dataset): + assert isinstance(target, Dataset) + for subject, predicate, obj, context in source.quads((None, None, None, None)): + context_graph = ( + target.default_graph if context == source.default_graph.identifier else target.graph(context) + ) + context_graph.add((subject, predicate, obj)) + else: + for triple in source: + target.add(triple) + return target + + +def prepare_data_graph( + data_graph: Graph, + transformer_identifiers: Collection[URIRef] | None = None, +) -> Graph: + """Run selected transformers on one transient copy of a data graph. + + Passing ``None`` preserves the low-level helper's original behaviour and + runs every registered transformer. The caller-owned source graph is never + passed to transformer code. + """ + load_transformers() + return run_registered_transformers(_copy_data_graph(data_graph), transformer_identifiers) + + +def run_registered_transformers( + data_graph: Graph, + identifiers: Collection[URIRef] | None = None, +) -> Graph: + """Run the selected registered transformers sequentially.""" + transformed_graph = data_graph + for registered in get_registered_transformers(identifiers): + # Class transformers are deliberately instantiated without arguments; + # persistent state must not leak between separate validation runs. + implementation = registered.implementation + if isinstance(implementation, type): + transformed_graph = implementation().transform(transformed_graph) + else: + transformed_graph = implementation(transformed_graph) + if not isinstance(transformed_graph, Graph): + raise TypeError(f"Graph transformer {registered.name} did not return an RDFLib Graph") + return transformed_graph + + +__all__ = ["prepare_data_graph", "run_registered_transformers"] diff --git a/rocrate_validator/requirements/shacl/transformers/preparation.py b/rocrate_validator/requirements/shacl/transformers/preparation.py new file mode 100644 index 000000000..3157a0b14 --- /dev/null +++ b/rocrate_validator/requirements/shacl/transformers/preparation.py @@ -0,0 +1,213 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""SHACL integration for graph-transformer-dependent validation.""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass +from typing import cast + +from pyshacl.monkey import rdflib_bool_patch, rdflib_bool_unpatch +from pyshacl.rdfutil.load import load_from_source +from rdflib import RDF, BNode, Dataset, Graph, Literal, Namespace, URIRef +from rdflib.collection import Collection + +from rocrate_validator.constants import SHACL_NS +from rocrate_validator.requirements.shacl.transformers.discovery import load_transformers +from rocrate_validator.requirements.shacl.transformers.pipeline import prepare_data_graph +from rocrate_validator.requirements.shacl.transformers.registry import get_registered_transformers +from rocrate_validator.requirements.shacl.transformers.vocabulary import ( + REQUIRES_GRAPH_TRANSFORMER_PREDICATE, +) + +logger = logging.getLogger(__name__) + +GraphSource = Graph | str | bytes +ShapesSource = GraphSource | None + + +class GraphTransformerCompatibilityError(ValueError): + """Raised when pySHACL options cannot support requested transformers.""" + + +@dataclass(frozen=True) +class PreparedValidationGraphs: + """Data and shapes sources prepared for one pySHACL invocation.""" + + data_graph: GraphSource + shapes_graph: ShapesSource + + +def prepare_validation_graphs( + data_graph: GraphSource, + shapes_graph: ShapesSource, + *, + data_graph_format: str | None = None, + shacl_graph_format: str | None = None, + do_owl_imports: bool | int = False, + inplace: bool = False, + sparql_mode: bool = False, +) -> PreparedValidationGraphs: + """Apply graph transformers explicitly requested by SHACL shapes. + + Shapes without a transformer declaration leave caller-owned graph objects + untouched. Transformer-dependent validation receives private data and + shapes copies suitable for a single pySHACL invocation. + """ + inspected_shapes = _load_shapes_for_inspection( + shapes_graph, + rdf_format=shacl_graph_format, + do_owl_imports=do_owl_imports, + ) + if inspected_shapes is None: + return PreparedValidationGraphs(data_graph, shapes_graph) + + required_transformers = _get_required_transformers(inspected_shapes) + if not required_transformers: + # Reuse an already parsed serialized source instead of asking pySHACL + # to parse it a second time. Existing Graph inputs remain unchanged. + return PreparedValidationGraphs(data_graph, inspected_shapes) + + _validate_options(required_transformers, inplace=inplace, sparql_mode=sparql_mode) + annotation_predicates = _get_annotation_predicates(required_transformers) + prepared_shapes = _copy_shapes_graph(inspected_shapes) + _ignore_annotations_in_closed_shapes(prepared_shapes, annotation_predicates) + prepared_data = _load_data_graph(data_graph, rdf_format=data_graph_format) + prepared_data = prepare_data_graph(prepared_data, required_transformers) + return PreparedValidationGraphs(prepared_data, prepared_shapes) + + +def _load_shapes_for_inspection( + source: ShapesSource, + *, + rdf_format: str | None, + do_owl_imports: bool | int, +) -> Graph | None: + if source is None: + return None + if isinstance(source, Graph) and not do_owl_imports: + return source + + load_source: GraphSource = _copy_shapes_graph(source) if isinstance(source, Graph) else source + rdflib_bool_patch() + try: + return cast( + "Graph", + load_from_source( + load_source, + rdf_format=rdf_format, + multigraph=True, + do_owl_imports=do_owl_imports, + logger=logger, + ), + ) + finally: + rdflib_bool_unpatch() + + +def _load_data_graph(source: GraphSource, *, rdf_format: str | None) -> Graph: + if isinstance(source, Graph): + return source + return cast( + "Graph", + load_from_source( + source, + rdf_format=rdf_format, + multigraph=True, + do_owl_imports=False, + logger=logger, + ), + ) + + +def _copy_shapes_graph(source: Graph) -> Graph: + """Flatten a shapes source into an isolated graph for private changes.""" + identifier = source.default_graph.identifier if isinstance(source, Dataset) else source.identifier + target = Graph(identifier=identifier, base=source.base) + for prefix, namespace in source.namespaces(): + target.bind(prefix, namespace, replace=True) + if isinstance(source, Dataset): + for subject, predicate, obj, _ in source.quads((None, None, None, None)): + target.add((subject, predicate, obj)) + else: + for triple in source: + target.add(triple) + return target + + +def _get_required_transformers(shapes_graph: Graph) -> frozenset[URIRef]: + identifiers: set[URIRef] = set() + for identifier in shapes_graph.objects(None, REQUIRES_GRAPH_TRANSFORMER_PREDICATE): + if not isinstance(identifier, URIRef): + raise TypeError("Graph transformer identifiers declared by SHACL shapes must be IRIs") + identifiers.add(identifier) + return frozenset(identifiers) + + +def _get_annotation_predicates(transformer_identifiers: frozenset[URIRef]) -> frozenset[URIRef]: + load_transformers() + return frozenset( + predicate + for transformer in get_registered_transformers(transformer_identifiers) + for predicate in transformer.annotation_predicates + ) + + +def _validate_options( + required_transformers: frozenset[URIRef], + *, + inplace: bool, + sparql_mode: bool, +) -> None: + if not required_transformers: + return + if sparql_mode: + raise GraphTransformerCompatibilityError( + "sparql_mode cannot be used with graph-transformer-dependent SHACL shapes" + ) + if inplace: + raise GraphTransformerCompatibilityError( + "inplace=True cannot be used when SHACL shapes require graph transformers" + ) + + +def _ignore_annotations_in_closed_shapes( + shapes_graph: Graph, + annotation_predicates: frozenset[URIRef], +) -> None: + if not annotation_predicates: + return + + shacl_ns = Namespace(SHACL_NS) + for shape in shapes_graph.subjects(shacl_ns.closed, Literal(True)): + list_head = shapes_graph.value(shape, shacl_ns.ignoredProperties) + if list_head is None or list_head == RDF.nil: + if list_head == RDF.nil: + shapes_graph.remove((shape, shacl_ns.ignoredProperties, RDF.nil)) + list_head = BNode() + shapes_graph.add((shape, shacl_ns.ignoredProperties, list_head)) + ignored_properties = Collection(shapes_graph, list_head) + existing = set(ignored_properties) + for predicate in sorted(annotation_predicates, key=str): + if predicate not in existing: + ignored_properties.append(predicate) + + +__all__ = [ + "GraphTransformerCompatibilityError", + "PreparedValidationGraphs", + "prepare_validation_graphs", +] diff --git a/rocrate_validator/requirements/shacl/transformers/registry.py b/rocrate_validator/requirements/shacl/transformers/registry.py new file mode 100644 index 000000000..fe5f8942a --- /dev/null +++ b/rocrate_validator/requirements/shacl/transformers/registry.py @@ -0,0 +1,110 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Registration metadata for SHACL graph transformers.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Callable, Collection + + from rdflib import URIRef + + from rocrate_validator.requirements.shacl.transformers.base import GraphTransformer, TransformerFunction + + +@dataclass(frozen=True) +class RegisteredTransformer: + """A transformer definition together with its execution metadata.""" + + identifier: URIRef + name: str + order: int + annotation_predicates: frozenset[URIRef] + implementation: TransformerFunction | type[GraphTransformer] + + +# Qualified names make registration idempotent when Python imports or reloads +# a transformer module more than once. A reload replaces the same definition +# instead of scheduling it twice. +_TRANSFORMERS: dict[str, RegisteredTransformer] = {} + + +def register_transformer( + implementation: TransformerFunction | type[GraphTransformer], + identifier: URIRef, + order: int, + annotation_predicates: Collection[URIRef] = (), +) -> None: + """Register one implementation and reject identifier collisions.""" + name = f"{implementation.__module__}.{implementation.__qualname__}" + duplicate = next( + ( + transformer + for registered_name, transformer in _TRANSFORMERS.items() + if transformer.identifier == identifier and registered_name != name + ), + None, + ) + if duplicate is not None: + raise ValueError(f"Graph transformer identifier {identifier} is already registered by {duplicate.name}") + _TRANSFORMERS[name] = RegisteredTransformer( + identifier, + name, + order, + frozenset(annotation_predicates), + implementation, + ) + + +def graph_transformer( + *, + identifier: URIRef, + order: int = 100, + annotation_predicates: Collection[URIRef] = (), +) -> Callable[[TransformerFunction], TransformerFunction]: + """Register a function as a graph transformer.""" + + def decorator(function: TransformerFunction) -> TransformerFunction: + register_transformer(function, identifier, order, annotation_predicates) + return function + + return decorator + + +def get_registered_transformers( + identifiers: Collection[URIRef] | None = None, +) -> tuple[RegisteredTransformer, ...]: + """Return selected transformers in deterministic execution order.""" + transformers = tuple(_TRANSFORMERS.values()) + if identifiers is not None: + requested = set(identifiers) + registered = {item.identifier for item in transformers} + unknown = requested - registered + if unknown: + unknown_list = ", ".join(sorted(map(str, unknown))) + raise ValueError(f"Unknown graph transformer(s): {unknown_list}") + transformers = tuple(item for item in transformers if item.identifier in requested) + return tuple(sorted(transformers, key=lambda item: (item.order, item.name))) + + +__all__ = [ + "RegisteredTransformer", + "get_registered_transformers", + "graph_transformer", + "register_transformer", +] diff --git a/rocrate_validator/requirements/shacl/transformers/vocabulary.py b/rocrate_validator/requirements/shacl/transformers/vocabulary.py new file mode 100644 index 000000000..d80c79674 --- /dev/null +++ b/rocrate_validator/requirements/shacl/transformers/vocabulary.py @@ -0,0 +1,23 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rdflib import Namespace, URIRef + +# Shapes request bundled transformers explicitly through this predicate. This +# keeps private annotations and graph-copying overhead out of unrelated SHACL +# validations. +VALIDATOR_NS = Namespace("https://github.com/crs4/rocrate-validator/") +REQUIRES_GRAPH_TRANSFORMER_PREDICATE: URIRef = URIRef(VALIDATOR_NS.requiresGraphTransformer) + +__all__ = ["REQUIRES_GRAPH_TRANSFORMER_PREDICATE"] diff --git a/rocrate_validator/requirements/shacl/utils.py b/rocrate_validator/requirements/shacl/utils.py index ad2a0b8fc..d518ca352 100644 --- a/rocrate_validator/requirements/shacl/utils.py +++ b/rocrate_validator/requirements/shacl/utils.py @@ -27,6 +27,7 @@ from rocrate_validator.errors import BadSyntaxError from rocrate_validator.models import Severity from rocrate_validator.utils import log as logging +from rocrate_validator.utils.rdf import parse_turtle_with_relative_iris if TYPE_CHECKING: from rocrate_validator.requirements.shacl.models import Shape @@ -171,11 +172,18 @@ def __init__( property_shapes: list[Node], shapes_graphs: dict[Node, Graph], shapes_graph: Graph, + relative_iris: set[Node] | None = None, ): self._node_shapes = node_shapes self._property_shapes = property_shapes self._shapes_graph = shapes_graph self._shapes_graphs = shapes_graphs + self._relative_iris = set() if relative_iris is None else relative_iris + + @property + def relative_iris(self) -> set[Node]: + """Return IRIs whose Turtle spelling was relative before RDFLib resolved it.""" + return self._relative_iris.copy() @property def node_shapes(self) -> list[Node]: @@ -290,15 +298,29 @@ def load_shapes_from_file(file_path: str, publicID: str | None = None) -> Shapes # Check the file path is not None assert file_path is not None, "The file path cannot be None" # Load the graph from the file - g = Graph() - g.parse(file_path, format="turtle", publicID=publicID) + g, relative_iris = parse_turtle_with_relative_iris(file_path, publicID) # Extract the shapes from the graph - return load_shapes_from_graph(g) + return load_shapes_from_graph(g, relative_iris=relative_iris) except Exception as e: - raise BadSyntaxError(str(e), file_path) from e + line = getattr(e, "lines", None) + line = line + 1 if isinstance(line, int) else None + character = _get_syntax_error_character(e) + raise BadSyntaxError(str(e), file_path, line=line, character=character) from e + + +def _get_syntax_error_character(error: Exception) -> int | None: + """Return the one-based character offset from an RDFLib parser error.""" + index = getattr(error, "_i", None) + source = getattr(error, "_str", None) + if not isinstance(index, int) or not isinstance(source, bytes): + return None + + source_text = source.decode("utf-8", errors="replace") + line_start = source_text.rfind("\n", 0, index) + 1 + return index - line_start + 1 -def load_shapes_from_graph(g: Graph) -> ShapesList: +def load_shapes_from_graph(g: Graph, relative_iris: set[Node] | None = None) -> ShapesList: # define the SHACL namespace SHACL = Namespace(SHACL_NS) # find all NodeShapes @@ -320,7 +342,7 @@ def load_shapes_from_graph(g: Graph) -> ShapesList: subgraph.add((s, p, o)) subgraphs[shape] = subgraph - return ShapesList(node_shapes, property_shapes, subgraphs, g) + return ShapesList(node_shapes, property_shapes, subgraphs, g, relative_iris) def resolve_parent_shape(shapes_graph: Graph, source_shape_node: Node, shapes_registry) -> Shape | None: diff --git a/rocrate_validator/requirements/shacl/validator.py b/rocrate_validator/requirements/shacl/validator.py index dc4b73448..78615a047 100644 --- a/rocrate_validator/requirements/shacl/validator.py +++ b/rocrate_validator/requirements/shacl/validator.py @@ -15,36 +15,49 @@ # pylint: disable=cyclic-import # lazy imports break the cycle at runtime (see PLC0415 noqa markers in requirements) from __future__ import annotations -from pathlib import Path from typing import TYPE_CHECKING, Any, cast # pylint: disable=unused-import import pyshacl from rdflib import BNode, Graph, Literal, Namespace +from rdflib.namespace import OWL, RDF, RDFS from rdflib.term import Node, URIRef if TYPE_CHECKING: + from pathlib import Path + from pyshacl.pytypes import GraphLike from rocrate_validator.constants import ( - DEFAULT_ONTOLOGY_FILE, RDF_SERIALIZATION_FORMATS, RDF_SERIALIZATION_FORMATS_TYPES, SHACL_NS, VALID_INFERENCE_OPTIONS, VALID_INFERENCE_OPTIONS_TYPES, ) -from rocrate_validator.models import Profile, RequirementCheck, Severity, ValidationContext, ValidationResult +from rocrate_validator.models import ( + Profile, + RequirementCheck, + Severity, + ValidationContext, + ValidationResult, +) +from rocrate_validator.models.check_result import DeferredRequirementCheck +from rocrate_validator.requirements.shacl.errors import SHACLValidationError from rocrate_validator.requirements.shacl.models import ShapesRegistry +from rocrate_validator.requirements.shacl.transformers.preparation import ( + GraphTransformerCompatibilityError, + prepare_validation_graphs, +) from rocrate_validator.requirements.shacl.utils import make_uris_relative, map_severity from rocrate_validator.utils import log as logging -from rocrate_validator.utils.rdf import extract_base_from_jsonld +from rocrate_validator.utils.rdf import rebase_graph, rebase_node # set up logging logger = logging.getLogger(__name__) -class SHACLValidationSkip(Exception): - pass +class SHACLValidationSkip(DeferredRequirementCheck): + """Defer a source-profile check until the merged SHACL validation runs.""" class SHACLValidationAlreadyProcessed(Exception): @@ -68,9 +81,12 @@ def __enter__(self) -> SHACLValidationContext: ) logger.debug("Processing profile: %s (id: %s)", self._profile.name, self._profile.identifier) if self._profile.identifier != self._context.settings.profile_identifier: - logger.debug("Skipping validation of profile %s", self._profile.identifier) - self.context.result._add_skipped_check(self._check) - raise SHACLValidationSkip(f"Skipping validation of profile {self._profile.identifier}") + logger.debug("Deferring validation of profile %s", self._profile.identifier) + # This is a temporary control-flow skip. The target profile runs + # SHACL once over the merged shapes graph and records the check + # outcome afterwards. + self._shacl_context.deferred_checks.add(self._check) + raise SHACLValidationSkip(f"Deferring validation of profile {self._profile.identifier}") logger.debug("ValidationContext of profile %s initialized", self._profile.identifier) return self._shacl_context @@ -93,56 +109,113 @@ def check(self) -> RequirementCheck: class SHACLValidationContext(ValidationContext): def __init__(self, context: ValidationContext): + """Initialize a SHACL context shared by all profiles in ``context``.""" super().__init__(context.validator, context.settings) self._base_context: ValidationContext = context - # reference to the ontology path - self._ontology_path: Path | None = None - + # The SHACL adapter is a per-run context, but profile definitions are + # immutable preparation state. Reuse the base context's instances so + # target lookup and override handling cannot trigger another load. + self._profiles = context.profiles # reference to the contextual ShapeRegistry instance self._shapes_registry: ShapesRegistry = ShapesRegistry() # processed profiles self._processed_profiles: dict[str, bool] = {} + # profiles whose ontology and shapes have already been merged. A + # profile may expose several SHACL checks, but its graphs must be + # loaded only once before the combined validation is executed. + self._loaded_profiles: set[str] = set() + self._relative_shape_iris: set[Node] = set() + + # Checks awaiting the combined run have no result yet; they are not skips. + self.deferred_checks: set[RequirementCheck] = set() + # reference to the current validation profile self._current_validation_profile: Profile | None = None # store the validation result of the current profile (a pass/fail boolean) self._validation_result: bool | None = None - # reference to the contextual ontology graph - self._ontology_graph: Graph = Graph() + # pySHACL may mutate supplied graphs while applying inference/rules. + # Copy the prepared ontology into this run's working graph. + run_base_mappings = tuple( + (prepared, actual) for actual, prepared in context.prepared_base_mappings if actual != prepared + ) + self._run_base_mappings = run_base_mappings + self._ontology_graph = rebase_graph( + context.prepared_validation_plan.ontology_graph, + self._run_base_mappings, + rebase_nodes=context.prepared_validation_plan.ontology_relative_iris, + ) def __set_current_validation_profile__(self, profile: Profile) -> bool: - if profile.identifier not in self._processed_profiles: - # augment the ontology graph with the profile ontology - ontology_graph = self.__load_ontology_graph__(profile.path) - if ontology_graph: - self._ontology_graph += ontology_graph + """ + Select ``profile`` and load its graphs once into the shared context. + + Loading and processing are separate states: source-profile checks may + enter this context repeatedly while their execution is deferred to the + target profile. Ontology and shape graphs are therefore merged only on + the first visit, whereas selecting an already processed profile raises + :class:`SHACLValidationAlreadyProcessed`. + + :return: ``True`` when the profile is ready to be processed + :raises SHACLValidationAlreadyProcessed: If validation already ran for + ``profile`` + """ + if profile.identifier in self._processed_profiles: + raise SHACLValidationAlreadyProcessed(profile.identifier, self.get_validation_result(profile)) + + if profile.identifier not in self._loaded_profiles: # augment the shapes registry with the profile shapes profile_registry = ShapesRegistry.get_instance(profile) + self._relative_shape_iris.update(profile_registry.relative_iris) profile_shapes = profile_registry.get_shapes() profile_shapes_graph = profile_registry.shapes_graph + structural_nodes = { + subject + for subject, predicate, object_ in profile_shapes_graph + if str(predicate).startswith(SHACL_NS) or (predicate == RDF.type and str(object_).startswith(SHACL_NS)) + } + shacl_ns = Namespace(SHACL_NS) + profile_shapes_graph = rebase_graph( + profile_shapes_graph, + self._run_base_mappings, + preserve_nodes=structural_nodes, + rebase_nodes=profile_registry.relative_iris, + # A URI can identify both a shape and an RDF class. Keep its + # shape identifier stable in structural positions, while + # rebasing it when it supplies SHACL class semantics. + rebase_objects_for=(shacl_ns.targetClass, shacl_ns["class"]), + ) logger.debug("Loaded shapes: %s", profile_shapes) - # enable overriding of checks - if self.settings.allow_requirement_check_override: + # Filter shapes that must not participate in the combined SHACL run. + if self.settings.skip_checks or self.settings.allow_requirement_check_override: from rocrate_validator.requirements.shacl.requirements import SHACLRequirement # noqa: PLC0415 for requirement in [_ for _ in profile.requirements if isinstance(_, SHACLRequirement)]: for check in requirement.get_checks(): - if check.overridden and check.requirement.profile != self.target_profile: + if self.base_context.is_check_skipped(check): + profile_shapes_graph -= cast("Any", check).shape.graph + profile_shapes.pop(cast("Any", check).shape.key, None) + continue + if ( + self.settings.allow_requirement_check_override + and check.overridden + and check.requirement.profile != self.target_profile + ): profile_shapes_graph -= cast("Any", check).shape.graph - profile_shapes.pop(cast("Any", check).shape.key) + profile_shapes.pop(cast("Any", check).shape.key, None) # add the shapes to the registry self._shapes_registry.extend(profile_shapes, profile_shapes_graph) - # set the current validation profile - self._current_validation_profile = profile - # return True if the profile should be processed - return True - # return False if the profile has already been processed - return False + self._loaded_profiles.add(profile.identifier) + + # Select the profile on every entry: repeated source-profile checks are + # deferred individually even though their graphs are already loaded. + self._current_validation_profile = profile + return True def __unset_current_validation_profile__(self) -> None: self._current_validation_profile = None @@ -181,53 +254,29 @@ def shapes_registry(self) -> ShapesRegistry: @property def shapes_graph(self) -> Graph: - return self.shapes_registry.shapes_graph - - def __get_ontology_path__(self, profile_path: Path, ontology_filename: str = DEFAULT_ONTOLOGY_FILE) -> Path: - if not self._ontology_path: - self._ontology_path = Path(f"{profile_path}/{ontology_filename}") - return self._ontology_path - - def __get_data_graph_base__(self) -> str | None: - """ - Get the @base from the RO-Crate metadata JSON-LD. - - This extracts the @base from the @context of the data graph metadata, - which can be used to align the ontology graph's base URI with the data graph. - - :return: The @base value if found, None otherwise - """ - try: - metadata_dict = self.ro_crate.metadata.as_dict() - return extract_base_from_jsonld(metadata_dict) - except Exception as e: - logger.debug("Unable to extract @base from data graph metadata: %s", e) - return None - - def __load_ontology_graph__( - self, profile_path: Path, ontology_filename: str = DEFAULT_ONTOLOGY_FILE - ) -> Graph | None: - # load the graph of ontologies - ontology_graph: Graph | None = None - ontology_path = self.__get_ontology_path__(profile_path, ontology_filename) - if ontology_path.exists(): - logger.debug("Loading ontologies: %s", ontology_path) - ontology_graph = Graph() - - # Determine the publicID to use: - # 1. First, try to get @base from the data graph metadata - # 2. Fall back to the default publicID (RO-Crate URI) - data_graph_base = self.__get_data_graph_base__() - public_id = data_graph_base or self.publicID - - if data_graph_base: - logger.debug("Using @base from data graph metadata: %s", data_graph_base) - else: - logger.debug("Using default publicID: %s", self.publicID) - - ontology_graph.parse(ontology_path, format="ttl", publicID=public_id) - logger.debug("Ontologies loaded: %s", ontology_graph) - return ontology_graph + graph = self.shapes_registry.shapes_graph + # Resolve implicit targets after profiles have been merged: the class + # declaration and its metaclass definition may belong to different profiles. + # Include owl:Class, which pySHACL treats as a subclass of rdfs:Class. + class_types: set[Node] = {RDFS.Class, OWL.Class} + class_types.update(graph.subjects(RDFS.subClassOf, RDFS.Class)) + shacl = Namespace(SHACL_NS) + for shape in self.shapes_registry.get_shapes().values(): + node = shape.node + if node not in self._relative_shape_iris: + continue + target_class = rebase_node(node, self._run_base_mappings) + if target_class == node: + continue + for class_type in set(graph.objects(node, RDF.type)) & class_types: + # Retain the canonical shape identity for check/report lookup, + # but express its implicit class target using the real crate URI. + # Moving the class declaration also prevents pySHACL from adding + # an unintended second target at the canonical preparation URI. + graph.remove((node, RDF.type, class_type)) + graph.add((target_class, RDF.type, class_type)) + graph.add((node, shacl.targetClass, target_class)) + return graph @property def ontology_graph(self) -> Graph: @@ -405,6 +454,7 @@ def validate( self, # data to validate data_graph: GraphLike | str | bytes, + *, # validation settings abort_on_first: bool | None = True, advanced: bool | None = True, @@ -462,26 +512,40 @@ def validate( assert inference in (None, "rdfs", "owlrl", "both"), "Invalid inference option" - if isinstance(self._shapes_graph, Graph): - _inject_default_prefixes(self._shapes_graph) - # validate the data graph using pyshacl.validate - conforms, results_graph, results_text = pyshacl.validate( - data_graph, - shacl_graph=self.shapes_graph, - ont_graph=self.ont_graph, - inference=inference or ("owlrl" if self.ont_graph else None), - inplace=inplace, - abort_on_first=abort_on_first, - allow_infos=allow_infos, - allow_warnings=allow_warnings, - meta_shacl=meta_shacl, - iterate_rules=iterate_rules, - advanced=advanced, - js=False, - debug=False, - **kwargs, - ) + try: + prepared_graphs = prepare_validation_graphs( + data_graph, + self._shapes_graph, + data_graph_format=kwargs.get("data_graph_format"), + shacl_graph_format=kwargs.get("shacl_graph_format"), + do_owl_imports=kwargs.get("do_owl_imports", False), + inplace=bool(inplace), + sparql_mode=bool(kwargs.get("sparql_mode", False)), + ) + if isinstance(prepared_graphs.shapes_graph, Graph): + _inject_default_prefixes(prepared_graphs.shapes_graph) + + conforms, results_graph, results_text = pyshacl.validate( + prepared_graphs.data_graph, + shacl_graph=prepared_graphs.shapes_graph, + ont_graph=self.ont_graph, + inference=inference or ("owlrl" if self.ont_graph else None), + inplace=inplace, + abort_on_first=abort_on_first, + allow_infos=allow_infos, + allow_warnings=allow_warnings, + meta_shacl=meta_shacl, + iterate_rules=iterate_rules, + advanced=advanced, + js=False, + debug=False, + **kwargs, + ) + except GraphTransformerCompatibilityError: + raise + except Exception as e: + raise _to_shacl_validation_error(e) from e # log the validation results logger.debug("pyshacl.validate result: Conforms: %r", conforms) logger.debug("pyshacl.validate result: Results Graph: %r", results_graph) @@ -542,4 +606,12 @@ def _inject_default_prefixes(shapes_graph: Graph) -> None: shapes_graph.set((subject, shacl_ns.select, Literal(updated_query))) +def _to_shacl_validation_error(error: Exception) -> SHACLValidationError: + """Wrap graph preparation and engine failures in the public error type.""" + version = getattr(pyshacl, "__version__", "unknown") + return SHACLValidationError( + message=(f"SHACL validation could not be executed by pySHACL {version}: {type(error).__name__}: {error}") + ) + + __all__ = ["SHACLValidationResult", "SHACLValidator", "SHACLViolation"] diff --git a/rocrate_validator/rocrate/__init__.py b/rocrate_validator/rocrate/__init__.py index 2615e7b0a..978bc2c55 100644 --- a/rocrate_validator/rocrate/__init__.py +++ b/rocrate_validator/rocrate/__init__.py @@ -30,10 +30,12 @@ from .base import ROCrate from .entity import ROCrateEntity from .metadata import ROCrateMetadata +from .package_type import PackageType from .plain import ROCrateLocalFolder, ROCrateLocalZip, ROCrateRemoteZip __all__ = [ "BagitROCrate", + "PackageType", "ROCrate", "ROCrateBagitLocalFolder", "ROCrateBagitLocalZip", diff --git a/rocrate_validator/rocrate/bagit.py b/rocrate_validator/rocrate/bagit.py index 17149c46b..1ea46cdde 100644 --- a/rocrate_validator/rocrate/bagit.py +++ b/rocrate_validator/rocrate/bagit.py @@ -33,6 +33,10 @@ class BagitROCrate(ROCrate, ABC): + @property + def attached_descriptor_id(self) -> str: + return str(Path("data") / (self.relative_root_path or Path()) / "ro-crate-metadata.json") + def __init__(self, uri, relative_root_path=None): super().__init__(uri, relative_root_path) diff --git a/rocrate_validator/rocrate/base.py b/rocrate_validator/rocrate/base.py index e284b842c..eed7131e6 100644 --- a/rocrate_validator/rocrate/base.py +++ b/rocrate_validator/rocrate/base.py @@ -28,6 +28,7 @@ from .entity import ROCrateEntity from .metadata import ROCrateMetadata +from .package_type import PackageType, parse_packaging_mode if TYPE_CHECKING: from rdflib import Graph @@ -41,6 +42,12 @@ class ROCrate(ABC): Base class for representing and interacting with a Research Object Crate (RO-Crate). """ + # Set by the factory after selecting a concrete input adapter. They are + # declared here for static analysis; getattr-based defaults below preserve + # the behavior of directly constructed subclasses. + _package_type: PackageType + _packaging_mode_explicit: bool + def __new__(cls, uri: str | Path | URI, relative_root_path: Path | None = None): """ Factory method to create the appropriate ROCrate subclass instance. @@ -110,21 +117,31 @@ def metadata(self) -> ROCrateMetadata: self._metadata = ROCrateMetadata(self) return self._metadata + @property + def package_type(self) -> PackageType: + """Package context selected explicitly or assumed from the input source.""" + return getattr(self, "_package_type", PackageType.ATTACHED) + + @property + def packaging_mode_explicit(self) -> bool: + return getattr(self, "_packaging_mode_explicit", False) + def is_detached(self) -> bool: - root = self.metadata.get_root_data_entity() - if root and root.has_type("Dataset") and root.id == "./": - return False - if root and root.id_as_uri.is_remote_resource(): - # An absolute root @id doesn't necessarily mean detached; - # check if there are any local (non-web) data entities - local_data_entities = self.metadata.get_data_entities(exclude_web_data_entities=True) - return all(entity.id == root.id for entity in local_data_entities) - return False + return self.package_type is PackageType.DETACHED + + def is_attached(self) -> bool: + return self.package_type is PackageType.ATTACHED @property def metadata_descriptor_id(self) -> str: return ROCrateMetadata.METADATA_FILE_DESCRIPTOR + @property + def attached_descriptor_id(self) -> str: + """Canonical descriptor at the selected Attached package root.""" + root = self.relative_root_path or Path() + return str(root / ROCrateMetadata.METADATA_FILE_DESCRIPTOR) + @property @abstractmethod def size(self) -> int: @@ -343,36 +360,62 @@ def get_external_file_size(uri: str) -> int | None: return int(content_length) @staticmethod - def from_metadata_dict(metadata_dict: dict) -> ROCrate: + def from_metadata_dict( + metadata_dict: dict, + packaging_mode: str = "auto", + package_root: str | Path | URI | None = None, + metadata_only: bool = True, + ) -> ROCrate: """ - Create a new instance of the RO-Crate based on the metadata dictionary. + Create a crate from a metadata dictionary and an optional local package directory. :param metadata_dict: the metadata dictionary - :type metadata_dict: dict - - :raises ROCrateInvalidURIError: if the URI is invalid - """ - # create a new instance based on the URI (the ROCrate factory __new__ - # dispatches to a concrete subclass, so this is not truly abstract) - ro_crate = ROCrate(URI("./"), relative_root_path=None) # type: ignore[abstract] + :param packaging_mode: Explicit package context or an input-based default. + :param package_root: Local package directory that provides payload files. + :param metadata_only: Whether to skip checks that require package files. + """ + from .plain import ROCrateInMemory # noqa: PLC0415 + + mode = parse_packaging_mode(packaging_mode) + if package_root is not None: + if mode == "detached": + raise ValueError("A package_root cannot be used with detached packaging_mode") + ro_crate = ROCrate.new_instance( + package_root, + packaging_mode=mode, + metadata_only=metadata_only, + ) + else: + ro_crate = ROCrateInMemory("urn:rocrate-validator:in-memory/") + ro_crate._package_type = PackageType.UNSPECIFIED if mode == "auto" else PackageType(mode) + ro_crate._packaging_mode_explicit = mode != "auto" - # override the metadata with the provided dictionary + # Use the supplied graph while retaining the package's payload backend. ro_crate._metadata = ROCrateMetadata(ro_crate, metadata_dict=metadata_dict) return ro_crate @staticmethod - def new_instance(uri: str | Path | URI, relative_root_path: Path | None = None) -> ROCrate: + def new_instance( # noqa: C901 # pylint: disable=too-many-locals + uri: str | Path | URI, + relative_root_path: Path | None = None, + packaging_mode: str = "auto", + metadata_only: bool = False, + ) -> ROCrate: """ Create a new instance of the RO-Crate based on the URI. :param uri: the URI of the RO-Crate :type uri: Union[str, Path, URI] + :param packaging_mode: Explicit package context or an input-based default. + :param metadata_only: Allow Attached document input without a payload backend. + :return: a new instance of the RO-Crate :rtype: ROCrate :raises ROCrateInvalidURIError: if the URI is invalid """ + mode = parse_packaging_mode(packaging_mode) # Lazy imports break a cycle: bagit/plain inherit from this class, # but the factory needs runtime references to dispatch to them. from .bagit import ( # noqa: PLC0415 @@ -389,40 +432,69 @@ def new_instance(uri: str | Path | URI, relative_root_path: Path | None = None) ROCrateRemoteZip, ) - # check if the URI is valid - validate_rocrate_uri(uri, silent=False) - # create a new instance based on the URI + # Reject incompatible source/mode combinations before network access. if not isinstance(uri, URI): uri = URI(uri) - # check if the URI is a BagIt-wrapped crate - is_bagit_crate = BagitROCrate.is_bagit_wrapping_crate(uri) + if ( + mode == "attached" + and not metadata_only + and uri.is_remote_resource() + and Path(uri.get_path()).suffix.lower() != ".zip" + ): + raise ValueError("Attached mode for a remote standalone document requires a package adapter") + if mode == "detached" and (uri.is_local_directory() or Path(uri.get_path()).suffix.lower() == ".zip"): + raise ValueError("Detached mode requires a standalone metadata document, not a directory or ZIP") + # check if the URI is valid + validate_rocrate_uri(uri, silent=False) # check if the URI is a local directory if uri.is_local_directory(): - return ( + is_bagit_crate = BagitROCrate.is_bagit_wrapping_crate(uri) + crate: ROCrate = ( ROCrateBagitLocalFolder(uri, relative_root_path=relative_root_path) if is_bagit_crate else ROCrateLocalFolder(uri, relative_root_path=relative_root_path) ) + crate._package_type = PackageType.ATTACHED + crate._packaging_mode_explicit = mode != "auto" + return crate # check if the URI is a local zip file if uri.is_local_file(): suffix = uri.as_path().suffix.lower() if suffix == ".zip": - return ( + is_bagit_crate = BagitROCrate.is_bagit_wrapping_crate(uri) + crate = ( ROCrateBagitLocalZip(uri, relative_root_path=relative_root_path) if is_bagit_crate else ROCrateLocalZip(uri, relative_root_path=relative_root_path) ) - return ROCrateLocalMetadataFile(uri, relative_root_path=relative_root_path) + crate._package_type = PackageType.ATTACHED + crate._packaging_mode_explicit = mode != "auto" + return crate + if mode == "attached" and not metadata_only: + if uri.as_path().name != ROCrateMetadata.METADATA_FILE_DESCRIPTOR: + raise ValueError("Attached mode requires a canonical ro-crate-metadata.json or a package directory") + return ROCrate.new_instance(uri.as_path().parent, packaging_mode="attached") + crate = ROCrateLocalMetadataFile(uri, relative_root_path=relative_root_path) + crate._package_type = PackageType.ATTACHED if mode == "attached" else PackageType.DETACHED + crate._packaging_mode_explicit = mode != "auto" + return crate # check if the URI is a remote zip file if uri.is_remote_resource(): path_suffix = Path(uri.get_path()).suffix.lower() if path_suffix == ".zip": - return ( + is_bagit_crate = BagitROCrate.is_bagit_wrapping_crate(uri) + crate = ( ROCrateBagitRemoteZip(uri, relative_root_path=relative_root_path) if is_bagit_crate else ROCrateRemoteZip(uri, relative_root_path=relative_root_path) ) - return ROCrateRemoteMetadataFile(uri, relative_root_path=relative_root_path) + crate._package_type = PackageType.ATTACHED + crate._packaging_mode_explicit = mode != "auto" + return crate + crate = ROCrateRemoteMetadataFile(uri, relative_root_path=relative_root_path) + crate._package_type = PackageType.ATTACHED if mode == "attached" else PackageType.DETACHED + crate._packaging_mode_explicit = mode != "auto" + return crate # if the URI is not supported, raise an error raise ROCrateInvalidURIError(uri=uri, message="Unsupported RO-Crate URI") diff --git a/rocrate_validator/rocrate/entity.py b/rocrate_validator/rocrate/entity.py index 2c05bcef7..b3b31fd54 100644 --- a/rocrate_validator/rocrate/entity.py +++ b/rocrate_validator/rocrate/entity.py @@ -132,18 +132,27 @@ def id_as_path(self) -> Path: @classmethod def get_id_as_uri(cls, entity_id: str, ro_crate: ROCrate) -> URI: assert entity_id, "Entity ID cannot be None" + # JSON-LD resolves relative identifiers against an explicit document + # @base before they are interpreted as crate-relative payload paths. + # Keep the raw ID on the entity for reporting and graph lookups, while + # using its resolved form for local/remote classification. + resolved_id = ro_crate.metadata.resolve_identifier(entity_id) # Per RO-Crate 1.1 Β§ 4.2.2, an `@id` is either a relative URI path or # an external URI/IRI (RFC 3986/3987). External references are used # as-is (without resolving them against the crate URI) so the entity # is classified as remote/web-based; this covers both authority-based # forms (``http://``, ``scp://``) and scheme-only ones (``urn:``, # ``doi:``, ``arcp:``). - if is_external_reference(entity_id): - return URI(entity_id) + if is_external_reference(resolved_id): + return URI(resolved_id) # Otherwise the `@id` is a relative path: if the RO-Crate itself is # remote, resolve it against the crate URI so the entity is still # classified as remote/web-based. - if ro_crate.uri.is_remote_resource() and entity_id.startswith("./"): + if ( + ro_crate.uri.is_remote_resource() + and not getattr(ro_crate, "is_in_memory", False) + and entity_id.startswith("./") + ): return URI(f"{ro_crate.uri}/{entity_id[2:]}") return URI(cls.get_id_as_path(entity_id, ro_crate)) diff --git a/rocrate_validator/rocrate/metadata.py b/rocrate_validator/rocrate/metadata.py index 5e21a9879..5511731ff 100644 --- a/rocrate_validator/rocrate/metadata.py +++ b/rocrate_validator/rocrate/metadata.py @@ -17,10 +17,13 @@ import json from pathlib import Path from typing import TYPE_CHECKING, Any, cast +from urllib.parse import urljoin from rdflib import Graph +from rocrate_validator.errors import ROCrateMetadataNotFoundError from rocrate_validator.utils import log as logging +from rocrate_validator.utils.rdf import extract_base_from_jsonld from .entity import ROCrateEntity @@ -37,7 +40,7 @@ class ROCrateMetadata: def __init__(self, ro_crate: ROCrate, metadata_dict: dict | None = None) -> None: self._ro_crate = ro_crate self._dict = metadata_dict - self._json: str | None = json.dumps(metadata_dict) if metadata_dict else None + self._json: str | None = json.dumps(metadata_dict) if metadata_dict is not None else None self._graph: Graph | None = None @property @@ -76,12 +79,7 @@ def get_root_data_entity(self) -> ROCrateEntity: def get_root_data_entity_conforms_to(self) -> list[str] | None: try: root_data_entity = self.get_root_data_entity() - result = root_data_entity.get_property("conformsTo", []) - if result is None: - return None - if not isinstance(result, list): - result = [result] - return [_.id for _ in result] + return self._get_conforms_to(root_data_entity, "Root Data Entity") except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error getting entity image") @@ -90,8 +88,16 @@ def get_root_data_entity_conforms_to(self) -> list[str] | None: def get_main_workflow(self) -> ROCrateEntity: root_data_entity = self.get_root_data_entity() main_workflow = root_data_entity.get_property("mainEntity") - if not main_workflow: + if main_workflow is None: raise ValueError("no main workflow in metadata file descriptor") + if isinstance(main_workflow, list): + if len(main_workflow) != 1: + raise ValueError("mainEntity must contain exactly one entity") + main_workflow = main_workflow[0] + if not isinstance(main_workflow, ROCrateEntity): + raise TypeError("mainEntity must reference an entity") + if not isinstance(main_workflow.id, str) or not main_workflow.id: + raise ValueError("mainEntity must reference an entity with a valid @id") return main_workflow def get_entity(self, entity_id: str) -> ROCrateEntity | None: @@ -128,31 +134,67 @@ def get_web_data_entities(self) -> list[ROCrateEntity]: def get_conforms_to(self) -> list[str] | None: try: file_descriptor = self.get_file_descriptor_entity() - result = file_descriptor.get_property("conformsTo", []) - if result is None: - return None - if not isinstance(result, list): - result = [result] - return [_.id for _ in result] + return self._get_conforms_to(file_descriptor, "RO-Crate Metadata Descriptor") except Exception: if logger.isEnabledFor(logging.DEBUG): logger.exception("Error getting entity identifiers by type") return None - def as_json(self) -> str: - if not self._json: - self._json = cast( - "str", self.ro_crate.get_file_content(Path(self.ro_crate.metadata_descriptor_id), binary_mode=False) + @staticmethod + def _get_conforms_to(entity: ROCrateEntity, entity_description: str) -> list[str] | None: + """Get profile identifiers and explain values that cannot be used as references.""" + result = entity.get_property("conformsTo", []) + if result is None: + return None + values = result if isinstance(result, list) else [result] + identifiers: list[str] = [] + for value in values: + if isinstance(value, ROCrateEntity) and isinstance(value.id, str) and value.id: + identifiers.append(value.id) + continue + logger.warning( + "Cannot use %s `conformsTo` value %r for profile detection: expected a JSON-LD reference " + 'object containing `@id` (for example, {"@id": "https://example.org/profile"}).', + entity_description, + value, ) + return identifiers + + def __get_file_content__(self, binary_mode: bool) -> str | bytes: + """Read the metadata descriptor and identify a missing descriptor explicitly.""" + try: + return self.ro_crate.get_file_content(Path(self.ro_crate.metadata_descriptor_id), binary_mode=binary_mode) + except FileNotFoundError as e: + raise ROCrateMetadataNotFoundError( + message=str(e), + path=self.ro_crate.metadata_descriptor_id, + ) from e + + def as_bytes(self) -> bytes: + """Return the raw metadata descriptor content.""" + if self._dict is not None: + return self.as_json().encode("utf-8") + return cast("bytes", self.__get_file_content__(binary_mode=True)) + + def as_json(self) -> str: + if self._json is None: + self._json = cast("str", self.__get_file_content__(binary_mode=False)) return self._json def as_dict(self) -> dict[Any, Any]: - if not self._dict: + if self._dict is None: # if the dictionary is not cached, load it self._dict = json.loads(self.as_json()) assert self._dict is not None, "Metadata dictionary should not be None after loading" return self._dict + def resolve_identifier(self, identifier: str) -> str: + """Resolve an identifier using the metadata document's explicit JSON-LD ``@base``.""" + base = extract_base_from_jsonld(self.as_dict()) + if isinstance(base, str): + return urljoin(base, identifier) + return identifier + def as_graph(self, publicID: str | None = None) -> Graph: if not self._graph: # if the graph is not cached, load it diff --git a/rocrate_validator/rocrate/package_type.py b/rocrate_validator/rocrate/package_type.py new file mode 100644 index 000000000..c17c65ca8 --- /dev/null +++ b/rocrate_validator/rocrate/package_type.py @@ -0,0 +1,16 @@ +"""Processing context for an RO-Crate metadata document.""" + +from enum import Enum + + +class PackageType(str, Enum): + ATTACHED = "attached" + DETACHED = "detached" + UNSPECIFIED = "unspecified" + + +def parse_packaging_mode(value: str) -> str: + """Validate the public setting; ``auto`` is resolved by the source adapter.""" + if value not in {"auto", "attached", "detached"}: + raise ValueError("packaging_mode must be 'auto', 'attached', or 'detached'") + return value diff --git a/rocrate_validator/rocrate/plain.py b/rocrate_validator/rocrate/plain.py index 9be07f7ac..53c28a6ef 100644 --- a/rocrate_validator/rocrate/plain.py +++ b/rocrate_validator/rocrate/plain.py @@ -14,18 +14,17 @@ from __future__ import annotations -import io -import struct import zipfile from pathlib import Path from typing import TYPE_CHECKING from rocrate_validator.errors import ROCrateInvalidURIError from rocrate_validator.utils import log as logging -from rocrate_validator.utils.http import HttpRequester +from rocrate_validator.utils.http import HTTPRangeReader, HttpRequester from .base import ROCrate from .metadata import ROCrateMetadata +from .package_type import PackageType if TYPE_CHECKING: from rocrate_validator.utils.uri import URI @@ -34,6 +33,39 @@ logger = logging.getLogger(__name__) +class ROCrateInMemory(ROCrate): + """Metadata without an implicit payload directory or working-directory root.""" + + def __init__(self, uri: str = "urn:rocrate-validator:in-memory/"): + super().__init__(uri) + + @property + def is_in_memory(self) -> bool: + return True + + @property + def size(self) -> int: + return self.metadata.size + + def list_files(self) -> list[Path]: + return [] + + def has_descriptor(self) -> bool: + return True + + def has_file(self, path: Path) -> bool: + return False + + def has_directory(self, path: Path) -> bool: + return False + + def get_file_size(self, path: Path) -> int: + raise NotImplementedError("In-memory metadata has no payload storage") + + def get_file_content(self, path: Path, binary_mode: bool = True) -> str | bytes: + raise NotImplementedError("In-memory metadata has no payload storage") + + class ROCrateLocalFolder(ROCrate): """ Class representing an RO-Crate stored in a local folder. @@ -59,12 +91,16 @@ def metadata_descriptor_id(self) -> str: if self._metadata_descriptor_id: return self._metadata_descriptor_id base_path = self.uri.as_path() + canonical = self.attached_descriptor_id + if (base_path / canonical).is_file(): + self._metadata_descriptor_id = canonical + return canonical candidates = sorted( (p for p in base_path.rglob(f"*{ROCrateMetadata.METADATA_FILE_DESCRIPTOR}") if p.is_file()), key=lambda p: (len(p.relative_to(base_path).parts), str(p)), ) - if not candidates: - self._metadata_descriptor_id = ROCrateMetadata.METADATA_FILE_DESCRIPTOR + if len(candidates) != 1: + self._metadata_descriptor_id = canonical return self._metadata_descriptor_id self._metadata_descriptor_id = str(candidates[0].relative_to(base_path)) return self._metadata_descriptor_id @@ -158,12 +194,16 @@ def has_descriptor(self) -> bool: def metadata_descriptor_id(self) -> str: if self._metadata_descriptor_id: return self._metadata_descriptor_id + canonical = self.attached_descriptor_id + if Path(canonical) in self.list_files(): + self._metadata_descriptor_id = canonical + return canonical candidates = sorted( (p for p in self.list_files() if str(p).endswith(ROCrateMetadata.METADATA_FILE_DESCRIPTOR)), key=lambda p: (len(p.parts), str(p)), ) - if not candidates: - self._metadata_descriptor_id = ROCrateMetadata.METADATA_FILE_DESCRIPTOR + if len(candidates) != 1: + self._metadata_descriptor_id = canonical return self._metadata_descriptor_id self._metadata_descriptor_id = str(candidates[0]) return self._metadata_descriptor_id @@ -228,8 +268,9 @@ def __init__(self, path: str | Path | URI, relative_root_path: Path | None = Non if suffix not in (".json", ".jsonld"): raise ROCrateInvalidURIError(uri=path, message="Unsupported metadata file format") - def is_detached(self) -> bool: - return True + @property + def package_type(self) -> PackageType: + return getattr(self, "_package_type", PackageType.DETACHED) @property def metadata_descriptor_id(self) -> str: @@ -266,8 +307,9 @@ def __init__(self, uri: str | Path | URI, relative_root_path: Path | None = None if not self.uri.is_remote_resource(): raise ROCrateInvalidURIError(uri=uri) - def is_detached(self) -> bool: - return True + @property + def package_type(self) -> PackageType: + return getattr(self, "_package_type", PackageType.DETACHED) @property def metadata_descriptor_id(self) -> str: @@ -318,51 +360,24 @@ def __init_zip_reference__(self): if not self.uri.is_available(): raise ROCrateInvalidURIError(uri=url, message="URI is not available") - # Step 1: Fetch the last 22 bytes to find the EOCD record - eocd_data = self.__fetch_range__(url, -22, "") - - # Step 2: Find the EOCD record - eocd_offset = self.__find_eocd__(eocd_data) - - # Step 3: Fetch the EOCD and parse it - eocd_full_data = self.__fetch_range__(url, -22 - eocd_offset, -1) - central_directory_offset, central_directory_size = self.__parse_eocd__(eocd_full_data) - - # Step 4: Fetch the central directory - central_directory_data = self.__fetch_range__( - url, central_directory_offset, central_directory_offset + central_directory_size - 1 - ) - # Step 5: Parse the central directory and return the zip file - self._zipref = zipfile.ZipFile(io.BytesIO(central_directory_data)) # pylint: disable=consider-using-with + # Let zipfile handle directory offsets, comments and entry reads while + # translating its seeks into absolute HTTP ranges (never suffix ranges). + try: + self._zipref = zipfile.ZipFile( # pylint: disable=consider-using-with + HTTPRangeReader(url, self.size) + ) + except zipfile.BadZipFile as exc: + # zipfile masks OSError (including requests' HTTPError) while reading + # the directory. Preserve transport errors instead of reporting a bad ZIP. + if isinstance(exc.__context__, OSError): + raise exc.__context__ from None + raise @property def size(self) -> int: - response = HttpRequester().head(str(self.uri)) + response = HttpRequester().head(str(self.uri), allow_redirects=True, headers={"Accept-Encoding": "identity"}) response.raise_for_status() # Check if the request was successful file_size = response.headers.get("Content-Length") if file_size is not None: return int(file_size) raise ValueError("Could not determine the file size from the headers") - - @staticmethod - def __fetch_range__(uri: str, start, end): - headers = {"Range": f"bytes={start}-{end}"} - response = HttpRequester().get(uri, headers=headers) - response.raise_for_status() - return response.content - - @staticmethod - def __find_eocd__(data): - eocd_signature = b"PK\x05\x06" - eocd_offset = data.rfind(eocd_signature) - if eocd_offset == -1: - raise ValueError("EOCD not found") - return eocd_offset - - @staticmethod - def __parse_eocd__(data): - eocd_size = struct.calcsize("<4s4H2LH") - eocd = struct.unpack("<4s4H2LH", data[-eocd_size:]) - central_directory_size = eocd[5] - central_directory_offset = eocd[6] - return central_directory_offset, central_directory_size diff --git a/rocrate_validator/services.py b/rocrate_validator/services.py index 2462e440e..a832b677c 100644 --- a/rocrate_validator/services.py +++ b/rocrate_validator/services.py @@ -13,6 +13,7 @@ # limitations under the License. +import copy import shutil import tempfile import zipfile @@ -22,6 +23,7 @@ from rocrate_validator.errors import ProfileNotFound from rocrate_validator.events import Subscriber from rocrate_validator.models import Profile, Severity, ValidationResult, ValidationSettings, Validator +from rocrate_validator.models.profile_detection import ProfileDetectionResult from rocrate_validator.utils import log as logging from rocrate_validator.utils.http import HttpRequester from rocrate_validator.utils.paths import get_profiles_path @@ -35,12 +37,21 @@ def detect_profiles(settings: dict | ValidationSettings) -> list[Profile]: - # initialize the validator + """Detect profiles, preserving the historical list return type.""" + return detect_profiles_detailed(settings).profiles + + +def detect_profiles_detailed(settings: dict | ValidationSettings) -> ProfileDetectionResult: + """Detect profiles and return structured diagnostics and a confirmable suggestion.""" validator = __initialise_validator__(settings) - # detect the profiles - profiles = validator.detect_rocrate_profiles() - logger.debug("Profiles detected: %s", profiles) - return profiles + result = validator.detect_rocrate_profiles_detailed() + logger.debug("Detailed profile detection result: %s", result) + return result + + +def suggest_profile_for_string_conforms_to(settings: dict | ValidationSettings) -> Profile | None: + """Return a unique available profile matching a malformed literal conformsTo value.""" + return detect_profiles_detailed(settings).suggested_profile def validate_metadata_as_dict( @@ -51,15 +62,11 @@ def validate_metadata_as_dict( """ assert metadata_dict is not None, "Metadata dictionary cannot be None" assert isinstance(metadata_dict, dict), "Metadata must be a dictionary" - # set the RO-Crate metadata dictionary in the settings - if isinstance(settings, dict): - settings["metadata_dict"] = metadata_dict - settings["metadata_only"] = True - else: - settings.metadata_dict = metadata_dict - settings.metadata_only = True - # validate the RO-Crate metadata - return validate(settings, subscribers) + parsed_settings = ValidationSettings.parse(settings) + validator = _build_validator(parsed_settings, subscribers) + # Use per-run input instead of mutating caller-owned settings. This shares + # the same prepared-validator path used for directory-based validation. + return validator.validate(metadata_dict=metadata_dict) def validate(settings: dict | ValidationSettings, subscribers: list[Subscriber] | None = None) -> ValidationResult: @@ -98,18 +105,24 @@ def _extract_and_validate( settings: ValidationSettings, subscribers: list[Subscriber] | None, rocrate_path: Path ) -> Validator: """Extract a (local or downloaded) zipped RO-Crate to a temp dir and validate it.""" - original_data_path = settings.rocrate_uri - with tempfile.TemporaryDirectory() as tmp_dir: - try: - with zipfile.ZipFile(rocrate_path, "r") as zip_ref: - zip_ref.extractall(tmp_dir) - logger.debug("RO-Crate extracted to temporary directory: %s", tmp_dir) - settings.rocrate_uri = URI(str(tmp_dir)) - return _build_validator(settings, subscribers) - finally: - if original_data_path is not None: - settings.rocrate_uri = original_data_path - logger.debug("Original data path restored: %s", original_data_path) + # The extracted package must outlive this function and remain available + # through Validator.validate(); its owner is retained by the validator. + temp_dir = tempfile.TemporaryDirectory() # pylint: disable=consider-using-with + try: + with zipfile.ZipFile(rocrate_path, "r") as zip_ref: + zip_ref.extractall(temp_dir.name) + logger.debug("RO-Crate extracted to temporary directory: %s", temp_dir.name) + effective_settings = copy.copy(settings) + effective_settings.rocrate_uri = URI(temp_dir.name) + effective_settings._source_rocrate_uri = settings.rocrate_uri + validator = _build_validator(effective_settings, subscribers) + # ValidationContext is created by Validator.validate(), after this + # function returns. Keep the extracted package alive until then. + validator._extracted_crate_directory = temp_dir + return validator + except Exception: + temp_dir.cleanup() + raise def _download_remote_rocrate( @@ -117,7 +130,8 @@ def _download_remote_rocrate( ) -> Validator: """Download a remote (http/https/ftp) RO-Crate to a temp file, then extract and validate it.""" logger.debug("RO-Crate is a remote RO-Crate") - with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + with tempfile.TemporaryDirectory() as download_dir: + download_path = Path(download_dir) / "download.zip" requester = HttpRequester() offline = bool(getattr(settings, "offline", False)) # In offline mode, the cache is the only source of truth. Otherwise, @@ -138,13 +152,31 @@ def _download_remote_rocrate( raise FileNotFoundError( f"Failed to download remote RO-Crate '{rocrate_path.uri}' (status {r.status_code})." ) - with Path(tmp_file.name).open("wb") as f: + with download_path.open("wb") as f: shutil.copyfileobj(r.raw, f) - logger.debug("RO-Crate downloaded to temporary file: %s", tmp_file.name) - return _extract_and_validate(settings, subscribers, Path(tmp_file.name)) + logger.debug("RO-Crate downloaded to temporary file: %s", download_path) + return _extract_and_validate(settings, subscribers, download_path) -def __initialise_validator__( +def _initialise_dict_metadata_validator( + settings: ValidationSettings, subscribers: list[Subscriber] | None +) -> Validator: + """Build a validator for dictionary metadata, optionally using a package directory.""" + if settings.package_root is not None: + if settings.packaging_mode == "detached": + raise ValueError("package_root cannot be combined with detached packaging_mode") + if not settings.package_root.is_dir(): + raise ValueError(f"package_root is not a local directory: {settings.package_root}") + else: + if settings.packaging_mode == "attached" and not settings.metadata_only: + raise ValueError("Attached in-memory metadata requires metadata_only=True or package_root") + # A standalone dictionary has no payload backend, even when called + # through validate() rather than validate_metadata_as_dict(). + settings.metadata_only = True + return _build_validator(settings, subscribers) + + +def __initialise_validator__( # noqa: C901, PLR0911 # pylint: disable=too-many-return-statements settings: dict | ValidationSettings, subscribers: list[Subscriber] | None = None ) -> Validator: """ @@ -153,11 +185,29 @@ def __initialise_validator__( # if settings is a dict, convert to ValidationSettings settings = ValidationSettings.parse(settings) + if settings.package_root is not None and settings.metadata_dict is None: + raise ValueError("package_root requires metadata_dict") + + if settings.metadata_dict is not None: + return _initialise_dict_metadata_validator(settings, subscribers) + # parse the rocrate path assert settings.rocrate_uri is not None, "RO-Crate URI is required" rocrate_path: URI = URI(str(settings.rocrate_uri)) logger.debug("Validating RO-Crate: %s", rocrate_path) + if ( + settings.packaging_mode == "attached" + and not settings.metadata_only + and rocrate_path.is_remote_resource() + and (Path(rocrate_path.get_path()).suffix.lower() != ".zip") + ): + raise ValueError("Attached mode for a remote standalone document requires a package adapter") + if settings.packaging_mode == "detached" and ( + rocrate_path.is_local_directory() or Path(rocrate_path.get_path()).suffix.lower() == ".zip" + ): + raise ValueError("Detached mode requires a standalone metadata document, not a directory or ZIP") + # check if the RO-Crate exists if ( not getattr(settings, "metadata_only", False) @@ -175,6 +225,8 @@ def __initialise_validator__( # Resolve the RO-Crate source: remote URL, local ZIP, or local directory. # We support http/https/ftp protocols to download a remote RO-Crate. if rocrate_path.scheme in ("http", "https", "ftp"): + if Path(rocrate_path.get_path()).suffix.lower() != ".zip": + return _build_validator(settings, subscribers) return _download_remote_rocrate(settings, subscribers, rocrate_path) if rocrate_path.as_path().suffix == ".zip": logger.debug("RO-Crate is a local ZIP file") @@ -183,6 +235,8 @@ def __initialise_validator__( logger.debug("RO-Crate is a local directory") settings.rocrate_uri = URI(str(rocrate_path.as_path())) return _build_validator(settings, subscribers) + if rocrate_path.is_local_file() and rocrate_path.as_path().suffix.lower() in (".json", ".jsonld"): + return _build_validator(settings, subscribers) raise ValueError( f"Invalid RO-Crate URI: {rocrate_path}. It MUST be a local directory or a ZIP file (local or remote)." ) @@ -207,11 +261,11 @@ def get_profiles( :param allow_requirement_check_override: a flag to enable or disable the requirement check override (default: ``True``). If ``True``, the requirement check of a profile ``A`` can be overridden - by the requirement check of a profile extension ``B`` (i.e., when ``B extends A``) - if they share the same name. + by the requirement check of a direct profile extension ``B`` (i.e., when + ``B prof:isProfileOf A``) if they share the same name and severity. If ``False``, a profile extension ``B`` can only - add new requirements to the profile ``A`` (i.e., checks with name not present in ``A``) - and an error is raised if a check with the same name is found in both profiles. + add new requirements to the profile ``A`` (i.e., checks whose name and severity + pair is not present in ``A``), and an error is raised if an override is found. :type allow_requirement_check_override: bool :return: the list of profiles @@ -251,11 +305,11 @@ def get_profile( :param allow_requirement_check_override: a flag to enable or disable the requirement check override (default: ``True``). If ``True``, the requirement check of a profile ``A`` can be overridden - by the requirement check of a profile extension ``B`` (i.e., when ``B extends A``) - if they share the same name. + by the requirement check of a direct profile extension ``B`` (i.e., when + ``B prof:isProfileOf A``) if they share the same name and severity. If ``False``, a profile extension ``B`` can only - add new requirements to the profile ``A`` (i.e., checks with name not present in ``A``) - and an error is raised if a check with the same name is found in both profiles. + add new requirements to the profile ``A`` (i.e., checks whose name and severity + pair is not present in ``A``), and an error is raised if an override is found. :type allow_requirement_check_override: bool :return: the profile diff --git a/rocrate_validator/transformers/__init__.py b/rocrate_validator/transformers/__init__.py new file mode 100644 index 000000000..e368067f1 --- /dev/null +++ b/rocrate_validator/transformers/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Bundled graph-transformer implementations used by SHACL profiles.""" diff --git a/rocrate_validator/transformers/validation_candidate_marker.py b/rocrate_validator/transformers/validation_candidate_marker.py new file mode 100644 index 000000000..4f1800fb6 --- /dev/null +++ b/rocrate_validator/transformers/validation_candidate_marker.py @@ -0,0 +1,98 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import logging + +from rdflib import RDF, BNode, Dataset, Graph, Literal, Namespace, URIRef + +from rocrate_validator.requirements.shacl.transformers import graph_transformer + +logger = logging.getLogger(__name__) + +# Private terms owned by this implementation. They exist only in the transient +# graph passed to pySHACL and are never serialized back into the RO-Crate. +MARKER_NS = Namespace("https://github.com/crs4/rocrate-validator/graph-transformers/") +VALIDATION_CANDIDATE_PREDICATE: URIRef = URIRef(MARKER_NS.validationCandidate) +VALIDATION_CANDIDATE_TRANSFORMER: URIRef = URIRef(MARKER_NS.validationCandidateMarker) + +# These predicates point to an identifier, format, or vocabulary term without +# claiming that the object is an entity which the crate needs to describe. +REFERENCE_PREDICATES: frozenset[URIRef] = frozenset( + { + RDF.type, + URIRef("http://schema.org/propertyID"), + URIRef("http://schema.org/additionalType"), + URIRef("http://www.w3.org/ns/csvw#propertyUrl"), + URIRef("http://schema.org/inDefinedTermSet"), + URIRef("http://schema.org/encodingFormat"), + URIRef("http://purl.org/dc/terms/conformsTo"), + } +) + + +# The default-stage order leaves lower values available for transformers that +# must normalize or annotate the graph before candidate classification. +@graph_transformer( + identifier=VALIDATION_CANDIDATE_TRANSFORMER, + order=100, + annotation_predicates=(VALIDATION_CANDIDATE_PREDICATE,), +) +def mark_validation_candidates(data_graph: Graph) -> Graph: + """ + Mark validation candidates identified from the current data graph. + + Classification is based exclusively on assertions from the original + RO-Crate graph, before pySHACL adds ontology and inferred triples. A node is + kept in validation scope when the crate asserts any outgoing statement + about it, or when another entity points to it through a predicate that + claims it as an entity (for example ``schema:author`` or + ``schema:hasPart``). + + A positive marker keeps the validation scope stable when pySHACL later adds + ontology and inferred terms which were not visible in the original graph. + The orchestration layer provides a transient graph copy, so the validation + context's cached source graph is never mutated. + """ + described_or_claimed: set[URIRef | BNode] = set() + + triples = ( + ((subject, predicate, obj) for subject, predicate, obj, _ in data_graph.quads()) + if isinstance(data_graph, Dataset) + else iter(data_graph) + ) + for subject, predicate, obj in triples: + if isinstance(subject, (URIRef, BNode)): + described_or_claimed.add(subject) + if isinstance(obj, (URIRef, BNode)) and predicate not in REFERENCE_PREDICATES: + described_or_claimed.add(obj) + + marker_value = Literal(True) + target_graph = data_graph.default_graph if isinstance(data_graph, Dataset) else data_graph + for node in described_or_claimed: + target_graph.add((node, VALIDATION_CANDIDATE_PREDICATE, marker_value)) + + logger.debug( + "Prepared validation graph with %d validation-candidate markers", + len(described_or_claimed), + ) + return data_graph + + +__all__ = [ + "VALIDATION_CANDIDATE_PREDICATE", + "VALIDATION_CANDIDATE_TRANSFORMER", + "mark_validation_candidates", +] diff --git a/rocrate_validator/utils/cache_warmup.py b/rocrate_validator/utils/cache_warmup.py index b2dceff29..5eef9e880 100644 --- a/rocrate_validator/utils/cache_warmup.py +++ b/rocrate_validator/utils/cache_warmup.py @@ -32,6 +32,7 @@ from rocrate_validator import constants from rocrate_validator.utils import log as logging from rocrate_validator.utils.http import OFFLINE_CACHE_MISS_STATUS, HttpRequester +from rocrate_validator.utils.paths import get_profiles_path if TYPE_CHECKING: from collections.abc import Iterable, Sequence @@ -194,7 +195,6 @@ def _find_profile(identifier, settings) -> Profile | None: """ # Import here to avoid a circular import with models.py. from rocrate_validator.models import Profile # noqa: PLC0415 - from rocrate_validator.utils.paths import get_profiles_path # noqa: PLC0415 # Load profiles to ensure the requested one is available and its graph is parsed. global __profiles_loaded # noqa: PLW0603 diff --git a/rocrate_validator/utils/http.py b/rocrate_validator/utils/http.py index 7b3e44fd6..ecf0ebe9d 100644 --- a/rocrate_validator/utils/http.py +++ b/rocrate_validator/utils/http.py @@ -16,9 +16,11 @@ import atexit import contextlib +import io import random import string import threading +from http import HTTPStatus from pathlib import Path from typing import TYPE_CHECKING, Any @@ -433,6 +435,78 @@ def reset(cls) -> None: cls._instance = None +class HTTPRangeReader(io.RawIOBase): + """ + Seekable HTTP input using bounded ranges and a caller-supplied resource size. + + Uses HttpRequester for transport and retains the full response when a server + ignores Range, so subsequent reads can be served without further downloads. + """ + + def __init__(self, url: str, size: int): + super().__init__() + self._url = url + self._size = size + self._position = 0 + self._full_content: bytes | None = None + + def readable(self) -> bool: + return True + + def seekable(self) -> bool: + return True + + def tell(self) -> int: + return self._position + + def seek(self, offset: int, whence: int = io.SEEK_SET) -> int: + if whence == io.SEEK_CUR: + offset += self._position + elif whence == io.SEEK_END: + offset += self._size + elif whence != io.SEEK_SET: + raise ValueError(f"Invalid whence: {whence}") + if offset < 0: + raise ValueError("Negative seek position") + self._position = offset + return offset + + def read(self, size: int = -1) -> bytes: + start = self._position + stop = self._size if size < 0 else min(start + size, self._size) + if start >= stop: + return b"" + + if self._full_content is None: + response = HttpRequester().get( + self._url, + headers={"Range": f"bytes={start}-{stop - 1}", "Accept-Encoding": "identity"}, + ) + response.raise_for_status() + if response.status_code == HTTPStatus.OK and len(response.content) == self._size: + # Some servers ignore Range. Retain the complete response so later + # seeks do not download the archive again, and slice at absolute offsets. + logger.warning( + "Server ignored HTTP Range for %s; received the entire resource (%d bytes), " + "which will be retained in memory.", + self._url, + self._size, + ) + self._full_content = response.content + elif ( + response.status_code == HTTPStatus.PARTIAL_CONTENT + and response.headers.get("Content-Range") == f"bytes {start}-{stop - 1}/{self._size}" + and len(response.content) == stop - start + ): + self._position = stop + return response.content + else: + raise OSError(f"Invalid HTTP range response for {self._url}") + + self._position = stop + return self._full_content[start:stop] + + class _OfflineFallbackSession: """ Minimal session used when offline mode is requested but no HTTP cache diff --git a/rocrate_validator/utils/io_helpers/output/json/formatters.py b/rocrate_validator/utils/io_helpers/output/json/formatters.py index ac62ddd33..b39addbd4 100644 --- a/rocrate_validator/utils/io_helpers/output/json/formatters.py +++ b/rocrate_validator/utils/io_helpers/output/json/formatters.py @@ -62,6 +62,8 @@ def format_validation_results( # Extract settings from the first result if available settings = results[0].validation_settings json_output["validation_settings"] = settings.to_dict() + json_output["validation_settings"]["package_type"] = results[0].context.ro_crate.package_type.value + json_output["validation_settings"]["packaging_mode_explicit"] = results[0].context.ro_crate.packaging_mode_explicit # Determine verbosity from settings verbose = settings.verbose if settings else False @@ -71,6 +73,9 @@ def format_validation_results( # Initialize the overall passed status json_output["passed"] = True + json_output["skipped_checks"] = 0 + json_output["skipped_check_details"] = [] + json_output["extra_checks"] = [] # Initialize the profile results dictionary results_key = "validation_results_by_profile" @@ -90,10 +95,13 @@ def format_validation_results( json_output[results_key][profile_identifier] = result_dict # Update the overall passed status json_output["passed"] = json_output["passed"] and result.passed() + json_output["skipped_checks"] += result.skipped_checks_count + json_output["skipped_check_details"].extend(detail.to_dict() for detail in result.skipped_check_details) # Update the overall list of issues if "issues" not in json_output: json_output["issues"] = [] json_output["issues"].extend(result_dict.get("issues", [])) + json_output["extra_checks"].extend(result_dict.get("extra_checks", [])) # Add overall statistics stats_dict = _compute_overall_statistics(results, verbose=verbose) diff --git a/rocrate_validator/utils/io_helpers/output/text/formatters.py b/rocrate_validator/utils/io_helpers/output/text/formatters.py index ce753b75b..fb8eb0b4e 100644 --- a/rocrate_validator/utils/io_helpers/output/text/formatters.py +++ b/rocrate_validator/utils/io_helpers/output/text/formatters.py @@ -19,6 +19,8 @@ from rich.align import Align from rich.markdown import Markdown from rich.padding import Padding +from rich.table import Table +from rich.text import Text from rocrate_validator.utils import log as logging from rocrate_validator.utils.io_helpers.colors import get_severity_color @@ -45,26 +47,38 @@ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderR # Print validation details # Print the list of failed requirements yield Padding("\n[bold]The following requirements have not been met: [/bold]", (0, 2)) + context = result.context for requirement in sorted(result.failed_requirements, key=lambda x: x.identifier): - yield Align(f"\n[profile: [magenta bold]{requirement.profile.name}[/magenta bold]]", align="right") + failed_checks = sorted( + result.get_failed_checks_by_requirement(requirement), + key=context.effective_check_identifier, + ) + if not failed_checks: + continue + + effective_profile = context.effective_check_profile(failed_checks[0]) + effective_requirement_identifier = context.effective_check_identifier(failed_checks[0]).rsplit(".", 1)[0] + + yield Align(f"\n[profile: [magenta bold]{effective_profile.name}[/magenta bold]]", align="right") yield Padding( - f"[bold][cyan][u][ {requirement.identifier} ]: {Markdown(requirement.name).markup}[/u][/cyan][/bold]", + f"[bold][cyan][u][ {effective_requirement_identifier} ]: " + f"{Markdown(requirement.name).markup}[/u][/cyan][/bold]", (0, 5), ) yield Padding(Markdown(requirement.description), (1, 6)) yield Padding("[white bold u] Failed checks [/white bold u]\n", (0, 8)) - for check in sorted( - result.get_failed_checks_by_requirement(requirement), key=lambda x: (-x.severity.value, x) - ): + failed_checks.sort(key=lambda check: (-check.severity.value, check)) + for check in failed_checks: issue_color = get_severity_color(check.level.severity) + effective_check_identifier = context.effective_check_identifier(check) yield Padding( - f"[bold][{issue_color}][ {check.identifier.center(16)} ][/{issue_color}] " - f"[magenta]{check.name}[/magenta][/bold]:", + f"[bold][{issue_color}][ {effective_check_identifier.center(16)} ][/{issue_color}] " + f"[hot_pink]{check.name}[/hot_pink][/bold]:", (1, 8, 0, 8), ) - yield Padding(Markdown(check.description), (0, 0, 0, len(check.identifier) + 13)) + yield Padding(Markdown(check.description), (0, 0, 0, len(effective_check_identifier) + 13)) yield Padding("[u] Detected issues [/u]", (0, 8)) for issue in sorted(result.get_issues_by_check(check), key=lambda x: (-x.severity.value, x)): path = "" @@ -83,6 +97,49 @@ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderR yield Padding("\n", (0, 0)) yield Padding("\n", (0, 0)) + yield from self._render_extra_checks(result) + + @staticmethod + def _render_extra_checks(result: ValidationResult) -> RenderResult: + extra_checks = result.extra_checks + if not extra_checks: + return + finding_count = len(extra_checks) + finding_label = "finding" if finding_count == 1 else "findings" + check_count = len({extra_check.identifier for extra_check in extra_checks}) + caption = Text(f"{finding_count} {finding_label}: ") + if finding_count == 1: + caption.append("this finding is advisory and does not affect conformance.", style="dim italic") + else: + caption.append("findings are advisory and do not affect conformance.", style="dim italic") + table = Table( + title=f"ExtraCheck Checks ({check_count})", + title_style="bold orange1", + border_style="orange1", + header_style="bold orange1", + show_lines=False, + caption=caption, + caption_justify="center", + ) + table.add_column("Profile", style="cyan", no_wrap=True) + table.add_column("Check", no_wrap=True) + table.add_column("Entity", style="cyan", overflow="fold") + table.add_column("Property", style="yellow", overflow="fold") + table.add_column("Value", style="green", overflow="fold") + table.add_column("Finding", overflow="fold") + + for extra_check in extra_checks: + table.add_row( + Text(result.context.effective_check_profile(extra_check.check).identifier, style="cyan"), + Text(extra_check.identifier, style=get_severity_color(extra_check.check.severity)), + extra_check.violating_entity or "β€”", + extra_check.violating_property or "β€”", + f'"{extra_check.value}"' if extra_check.value is not None else "β€”", + Text(extra_check.message), + ) + + yield Padding(table, (1, 2)) + class ValidationStatisticsTextOutputFormatter(OutputFormatter): def __init__(self, validation_statistics: ValidationStatistics): @@ -98,3 +155,13 @@ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderR if layout.layout is not None: yield layout.layout yield Padding("\n", (0, 0)) + + +class ValidationExtraChecksTextOutputFormatter(OutputFormatter): + """Render advisory extra_checks independently from conformance details.""" + + def __init__(self, validation_result: ValidationResult): + self._validation_result = validation_result + + def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: + yield from ValidationResultTextOutputFormatter._render_extra_checks(self._validation_result) diff --git a/rocrate_validator/utils/io_helpers/output/text/layout/progress.py b/rocrate_validator/utils/io_helpers/output/text/layout/progress.py index 5796960bb..2719fb83f 100644 --- a/rocrate_validator/utils/io_helpers/output/text/layout/progress.py +++ b/rocrate_validator/utils/io_helpers/output/text/layout/progress.py @@ -19,6 +19,7 @@ RequirementCheckValidationEvent, RequirementValidationEvent, ValidationContext, + ValidationEvent, ValidationSettings, ValidationStatistics, ) @@ -59,9 +60,16 @@ def __init__(self, settings: dict | ValidationSettings, stats: ValidationStatist self.REQUIREMENT_CHECK_VALIDATION, total=stats.total_checks ) # Initialize progress according to current statistics - self.__progress.update(task_id=self.profile_validation, advance=len(stats.validated_profiles)) - self.__progress.update(task_id=self.requirement_validation, advance=len(stats.validated_requirements)) - self.__progress.update(task_id=self.requirement_check_validation, advance=len(stats.validated_checks)) + self._update_progress(stats) + + def _update_progress(self, stats: ValidationStatistics) -> None: + """Use the same unique, in-scope outcomes for live and final progress.""" + self.__progress.update(task_id=self.profile_validation, completed=len(stats.validated_profiles)) + self.__progress.update(task_id=self.requirement_validation, completed=len(stats.validated_requirements)) + self.__progress.update( + task_id=self.requirement_check_validation, + completed=len(stats.validated_checks) + stats.total_skipped_checks, + ) def start(self): self.__progress.start() @@ -77,13 +85,16 @@ def _on_requirement_check_validation_end( self, event: RequirementCheckValidationEvent, ctx: ValidationContext | None ) -> None: assert ctx is not None, "Validation context must be provided" - # Only advance the progress for checks at or above the requested severity threshold, - # so the bar matches the set of checks actually reported. - if event.requirement_check.severity >= ctx.settings.requirement_severity: - self.__progress.update(task_id=self.requirement_check_validation, advance=1) + self._update_progress(ctx.result.statistics) def _on_requirement_validation_end(self, event: RequirementValidationEvent, ctx: ValidationContext | None) -> None: - self.__progress.update(task_id=self.requirement_validation, advance=1) + assert ctx is not None, "Validation context must be provided" + self._update_progress(ctx.result.statistics) def _on_profile_validation_end(self, event: ProfileValidationEvent, ctx: ValidationContext | None) -> None: - self.__progress.update(task_id=self.profile_validation, advance=1) + assert ctx is not None, "Validation context must be provided" + self._update_progress(ctx.result.statistics) + + def _on_validation_end(self, event: ValidationEvent, ctx: ValidationContext | None) -> None: + assert ctx is not None, "Validation context must be provided" + self._update_progress(ctx.result.statistics) diff --git a/rocrate_validator/utils/io_helpers/output/text/layout/report.py b/rocrate_validator/utils/io_helpers/output/text/layout/report.py index e31b13cf4..9b3ed48dd 100644 --- a/rocrate_validator/utils/io_helpers/output/text/layout/report.py +++ b/rocrate_validator/utils/io_helpers/output/text/layout/report.py @@ -12,6 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +# The output renderer depends on the crate entity model, whose concrete readers +# import shared report helpers; this existing package-level cycle is intentional. +# pylint: disable=cyclic-import + from __future__ import annotations import threading @@ -38,7 +42,6 @@ ) from rocrate_validator.utils import log as logging from rocrate_validator.utils.io_helpers.colors import get_severity_color -from rocrate_validator.utils.uri import URI from rocrate_validator.utils.versioning import get_version from .dispatcher import EventDispatcher @@ -47,7 +50,7 @@ if TYPE_CHECKING: from collections.abc import Callable - from rocrate_validator.events import Subscriber + from rocrate_validator.events import Event, Subscriber from rocrate_validator.utils.io_helpers.output.console import Console # set up logging @@ -72,9 +75,12 @@ def __init__( self._validation_checks_progress: Layout | None = None self._progress_monitor: ProgressMonitor | None = None self._subscriber: EventDispatcher = _ReportLayoutSubscriber(self) + self.base_info_layout: Layout | None = None + self._resolved_package_type: str | None = None self.requirement_checks_container_layout: Layout | None = None self.passed_checks: Layout | None = None self.failed_checks: Layout | None = None + self.skipped_checks: Layout | None = None self.report_details_container: Layout | None = None self.overall_result: Layout | None = None self.requirement_checks_by_severity_container_layout: Any = None @@ -106,34 +112,24 @@ def live(self, update_callable: Callable) -> Any: return update_callable() def _init_layout(self): - - # Get the validation settings - settings = self.validation_settings - # Set the console height - self.console.height = 31 + self.console.height = 33 # Create the layout of the base info of the validation report - severity_color = get_severity_color(settings.requirement_severity) - base_info_layout = Layout( - Align( - f"\n[bold cyan]RO-Crate:[/bold cyan] [bold]{URI(str(settings.rocrate_uri)).uri}[/bold]" - "\n[bold cyan]Target Profile:[/bold cyan][bold magenta] " - f"{settings.profile_identifier}[/bold magenta] " - f"{'[italic](autodetected)[/italic]' if self.profile_autodetected else ''}" - f"\n[bold cyan]Validation Severity:[/bold cyan] " - f"[bold {severity_color}]{settings.requirement_severity}[/bold {severity_color}]", - style="white", - align="left", - ), + result = self.result or (self.statistics.validation_result if self.statistics else None) + if result: + self._resolved_package_type = result.context.ro_crate.package_type.value + self.base_info_layout = Layout( + self._base_info(), name="Base Info", - size=5, + size=7, ) self.passed_checks = Layout(name="PASSED") self.failed_checks = Layout(name="FAILED") + self.skipped_checks = Layout(name="SKIPPED") # Create the layout of the requirement checks section validated_checks_container = Layout(name="Requirement Checks Validated") - validated_checks_container.split_row(self.passed_checks, self.failed_checks) + validated_checks_container.split_row(self.passed_checks, self.failed_checks, self.skipped_checks) # Create the layout of the requirement checks section self.requirement_checks_by_severity_container_layout = Layout(name="Requirement Checks Validation", size=5) @@ -162,7 +158,7 @@ def _init_layout(self): # Create the layout of the report container report_container_layout = Layout(name="Report Container Layout") report_container_layout.split_column( - base_info_layout, + self.base_info_layout, Layout( Panel( requirement_checks_container_layout, @@ -200,14 +196,43 @@ def _init_layout(self): self.update_stats(self.statistics or ValidationStatistics(self.validation_settings)) # Extract the result if available - result = self.result or (self.statistics.validation_result) if self.statistics else None # Show the overall result if available if result: self.show_overall_result(result) + def _base_info(self) -> Align: + settings = self.validation_settings + severity_color = get_severity_color(settings.requirement_severity) + package_type = self._resolved_package_type or ( + settings.packaging_mode if settings.packaging_mode != "auto" else "resolving..." + ) + validation_scope = "Metadata Only" if settings.metadata_only else "Full Package" + crate_source = settings.package_root or getattr(settings, "_source_rocrate_uri", None) or settings.rocrate_uri + return Align( + f"\n[bold cyan]RO-Crate:[/bold cyan] " + f"[bold]{crate_source or 'in-memory metadata'}[/bold]" + f"\n[bold cyan]RO-Crate Package Type:[/bold cyan] [bold yellow]{package_type.title()}[/bold yellow]" + "\n[bold cyan]Target Profile:[/bold cyan][bold magenta] " + f"{settings.profile_identifier}[/bold magenta] " + f"{'[italic](autodetected)[/italic]' if self.profile_autodetected else ''}" + f"\n[bold cyan]Validation Severity:[/bold cyan] " + f"[bold {severity_color}]{settings.requirement_severity}[/bold {severity_color}]" + f"\n[bold cyan]Validation Scope:[/bold cyan] [bold orange1]{validation_scope}[/bold orange1]", + style="white", + align="left", + ) + + def set_package_type(self, package_type: str) -> None: + """Show the resolved package type as soon as the validation context exists.""" + self._resolved_package_type = package_type + if self.base_info_layout is not None: + self.base_info_layout.update(self._base_info()) + def update_stats(self, profile_stats: ValidationStatistics | None = None): assert profile_stats, "Profile stats must be provided" - assert self.passed_checks is not None and self.failed_checks is not None, "Layout not initialized" + assert self.passed_checks is not None and self.failed_checks is not None and self.skipped_checks is not None, ( + "Layout not initialized" + ) self.requirement_checks_by_severity_container_layout["required"].update( Panel( Align( @@ -265,10 +290,21 @@ def update_stats(self, profile_stats: ValidationStatistics | None = None): ) ) + self.skipped_checks.update( + Panel( + Align(str(len(profile_stats.skipped_checks)), align="center"), + padding=(1, 1), + title="SKIPPED Checks", + title_align="center", + border_style="yellow", + ) + ) + def show_overall_result(self, result: ValidationResult | None): assert result, "Validation result must be provided" assert self.overall_result is not None, "Layout not initialized" self.result = result + self.set_package_type(result.context.ro_crate.package_type.value) if result.passed(): icon = "[OK]" if not self.console.interactive else "βœ…" self.overall_result.update( @@ -302,6 +338,10 @@ def __init__(self, layout: ValidationReportLayout): super().__init__("ValidationReportLayout") self._layout = layout + def _on_validation_start(self, event: Event, ctx: ValidationContext | None) -> None: + assert ctx is not None, "Validation context must be provided" + self._layout.set_package_type(ctx.ro_crate.package_type.value) + def _on_requirement_check_validation_end( self, event: RequirementCheckValidationEvent, ctx: ValidationContext | None ) -> None: @@ -314,6 +354,8 @@ def _on_requirement_validation_end(self, event: RequirementValidationEvent, ctx: self._layout.update_stats(ctx.result.statistics) def _on_validation_end(self, event: ValidationEvent, ctx: ValidationContext | None) -> None: + assert ctx is not None, "Validation context must be provided" + self._layout.update_stats(ctx.result.statistics) self._layout.show_overall_result(event.validation_result) diff --git a/rocrate_validator/utils/jsonld.py b/rocrate_validator/utils/jsonld.py new file mode 100644 index 000000000..8d00a4edf --- /dev/null +++ b/rocrate_validator/utils/jsonld.py @@ -0,0 +1,205 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from collections.abc import Callable, Mapping +from typing import Any +from urllib.parse import urljoin + + +def _context_containers(context: object) -> dict[str, set[str]]: + """Return explicit array-preserving containers for locally defined terms.""" + containers: dict[str, set[str]] = {} + + if isinstance(context, list): + for item in context: + containers.update(_context_containers(item)) + elif isinstance(context, dict): + for term, definition in context.items(): + if not isinstance(term, str) or term.startswith("@"): + continue + if isinstance(definition, dict): + raw_container = definition.get("@container") + if isinstance(raw_container, str): + containers[term] = {raw_container} + elif isinstance(raw_container, list): + containers[term] = {value for value in raw_container if isinstance(value, str)} + else: + containers[term] = set() + else: + containers[term] = set() + + return containers + + +def find_singleton_property_arrays(metadata: Mapping[str, Any]) -> list[tuple[str | None, str]]: + """ + Find singleton arrays used for properties of flattened JSON-LD entities. + + The RO-Crate ``@graph`` array and ``@context`` are structural JSON-LD + arrays, not entity property values. JSON-LD keyword properties are also + excluded. Explicit ``@set`` and ``@list`` containers preserve array + semantics and must not be reported as non-compact property values. + """ + graph = metadata.get("@graph") + if not isinstance(graph, list): + return [] + + containers = _context_containers(metadata.get("@context")) + findings: list[tuple[str | None, str]] = [] + for entity in graph: + if not isinstance(entity, Mapping): + continue + entity_id = entity.get("@id") + if entity_id is not None and not isinstance(entity_id, str): + entity_id = str(entity_id) + for property_name, value in entity.items(): + if not isinstance(property_name, str) or property_name.startswith("@"): + continue + if ( + isinstance(value, list) + and len(value) == 1 + and not {"@set", "@list"}.intersection(containers.get(property_name, set())) + ): + findings.append((entity_id, property_name)) + return findings + + +_COMPACTION_KEYWORDS = frozenset({"@id", "@type", "@context", "@value", "@language"}) +_JSONLD_11 = 1.1 +_URI_GEN_DELIMS = (":", "/", "?", "#", "[", "]", "@") + + +def resolve_compaction_context( + context: object, + load_remote: Callable[[str], object], + *, + base: str = "", + remote_stack: tuple[str, ...] = (), +) -> dict[str, Any]: + """ + Keep ordered term definitions instead of losing prefix mappings in a set. + + Remote retrieval belongs to the calling profile, preserving its HTTP, + alternate-link and offline-cache behavior. Null contexts reset definitions; + null term definitions remain present so they can disable an earlier mapping. + """ + definitions: dict[str, Any] = {} + + def apply(source: object, source_base: str, stack: tuple[str, ...]) -> None: + if source is None: + definitions.clear() + elif isinstance(source, list): + for item in source: + apply(item, source_base, stack) + elif isinstance(source, str): + uri = urljoin(source_base, source) + if uri in stack: + raise ValueError(f"Recursive JSON-LD context: {uri}") + apply(load_remote(uri), uri, (*stack, uri)) + elif isinstance(source, dict): + definitions.update(source) + # Freeze namespace mappings when they are defined: redefining a + # referenced prefix in a later context must not change earlier IRIs. + for term, definition in source.items(): + iri = _mapped_iri(term, definitions) + if iri is not None: + definitions[term] = {**definition, "@id": iri} if isinstance(definition, dict) else iri + else: + raise TypeError("JSON-LD context must be an object, URL, array or null") + + apply(context, base, remote_stack) + return definitions + + +def _mapped_iri(term: str, definitions: Mapping[str, Any], seen: frozenset[str] = frozenset()) -> str | None: + """Resolve a mapping expressed using another declared prefix, without cycles.""" + if term in seen: + return None + definition = definitions.get(term) + iri = definition.get("@id") if isinstance(definition, dict) else definition + if not isinstance(iri, str) or iri.startswith(("@", "_:")): + return None + prefix, separator, suffix = iri.partition(":") + if separator and prefix in definitions and not suffix.startswith("//"): + namespace = _mapped_iri(prefix, definitions, seen | {term}) + return namespace + suffix if namespace else None + if not separator and iri in definitions: + return _mapped_iri(iri, definitions, seen | {term}) + return iri if separator else None + + +def _is_compact_iri(key: str, definitions: Mapping[str, Any]) -> bool: + prefix, separator, suffix = key.partition(":") + if not separator or prefix == "_" or suffix.startswith("//"): + return False + iri = _mapped_iri(prefix, definitions) + if iri is None: + return False + definition = definitions[prefix] + # For this check, contexts without @version use the JSON-LD 1.0 policy, + # matching the requirement for RO-Crate 1.1 and 1.2. This is a validator + # policy; it does not infer the mode selected by a JSON-LD processor: + # https://www.researchobject.org/ro-crate/specification/1.1/structure.html + # https://www.researchobject.org/ro-crate/specification/1.2/structure.html + # https://www.w3.org/TR/json-ld11/#json-ld-1-1-processing-mode + if definitions.get("@version", 1.0) != _JSONLD_11: + # In this check, a resolved mapping is enough to accept the key as a + # compact IRI. JSON-LD 1.1's @prefix flag is handled explicitly below. + return True + # JSON-LD 1.1 distinguishes simple (string) and expanded (object) term + # definitions when deciding whether a term can be used as a prefix: + # https://www.w3.org/TR/json-ld11/#compact-iris + if isinstance(definition, dict): + # An object such as {"@id": "https://example.org/"} needs @prefix: true + # to enable prefix use. Missing/false @prefix does not invalidate the + # term definition itself; it only prevents using it as a prefix here. + return definition.get("@prefix") is True + # A simple mapping such as "ex": "https://example.org/" needs no @prefix, + # but its IRI must end in a URI general delimiter (e.g. '/' or '#'). + return iri.endswith(_URI_GEN_DELIMS) + + +def find_unexpected_compaction_keys(entity: object, definitions: Mapping[str, Any]) -> dict[str, int]: + """ + Count unmapped properties in the flat RO-Crate JSON-LD representation. + + Keep the existing explicit-term policy (including rejection of unmapped + absolute IRI keys), but also accept eligible compact IRIs. Context and + literal payloads are not node objects and must not be traversed as such. + """ + unexpected: dict[str, int] = {} + + def visit(value: object) -> None: + if isinstance(value, list): + for item in value: + visit(item) + elif isinstance(value, dict): + for key, item in value.items(): + definition = definitions.get(key) + defined = definition is not None and not ( + isinstance(definition, dict) and "@id" in definition and definition["@id"] is None + ) + if ( + key not in _COMPACTION_KEYWORDS + and not defined + and (key in definitions or not _is_compact_iri(key, definitions)) + ): + unexpected[key] = unexpected.get(key, 0) + 1 + if key not in {"@context", "@value"}: + visit(item) + + visit(entity) + return unexpected diff --git a/rocrate_validator/utils/rdf.py b/rocrate_validator/utils/rdf.py index 6e4f1af12..d1464192e 100644 --- a/rocrate_validator/utils/rdf.py +++ b/rocrate_validator/utils/rdf.py @@ -12,7 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -from rdflib import Graph +import re +from collections.abc import Iterable, MutableSequence +from io import StringIO +from pathlib import Path +from posixpath import commonpath, relpath +from typing import Any +from urllib.parse import urljoin, urlsplit, urlunsplit + +from rdflib import Graph, URIRef +from rdflib.parser import create_input_source +from rdflib.plugins.parsers.notation3 import RDFSink, SinkParser +from rdflib.term import Node from rocrate_validator import constants from rocrate_validator.utils import log as logging @@ -22,6 +33,190 @@ logger = logging.getLogger(__name__) +PREPARED_PROFILE_BASE = "https://github.com/crs4/rocrate-validator/prepared/crate/" +_PREFIX_DECLARATION = re.compile(r"@?prefix\b\s+([^:\s]*):\s*<([^>]*)>", re.IGNORECASE) + + +def _prefix_declaration(argstr: str, index: int) -> tuple[str, str] | None: + directive_index = argstr[index:].lstrip() if index >= 0 else "" + match = _PREFIX_DECLARATION.match(directive_index) + if match is None: + return None + prefix, namespace = match.group(1), match.group(2) + assert prefix is not None and namespace is not None + return prefix, namespace + + +def _is_relative_iri(reference: str) -> bool: + parts = urlsplit(reference) + return not parts.scheme and not parts.netloc + + +class _TurtleProvenanceParser(SinkParser): + """RDFLib Turtle parser that records URI terms originating in relative syntax.""" + + def __init__(self, store: RDFSink, base_uri: str): + super().__init__(store, baseURI=base_uri, turtle=True) + self.relative_iris: set[Node] = set() + self.relative_prefixes: set[str] = set() + + def _record_prefix_declaration(self, declaration: tuple[str, str] | None, parsed_at: int) -> None: + if parsed_at < 0 or declaration is None: + return + prefix, namespace = declaration + if _is_relative_iri(namespace): + self.relative_prefixes.add(prefix) + else: + self.relative_prefixes.discard(prefix) + + def directive(self, argstr: str, i: int) -> int: + declaration = _prefix_declaration(argstr, i) + parsed_at = super().directive(argstr, i) + self._record_prefix_declaration(declaration, parsed_at) + return parsed_at + + def sparqlDirective(self, argstr: str, i: int) -> int: + declaration = _prefix_declaration(argstr, i) + parsed_at = super().sparqlDirective(argstr, i) + self._record_prefix_declaration(declaration, parsed_at) + return parsed_at + + def uri_ref2(self, argstr: str, i: int, res: MutableSequence[Any]) -> int: + qname: list = [] + self.qname(argstr, i, qname) + relative_qname = bool(qname and qname[0][0] in self.relative_prefixes) + start = self.skipSpace(argstr, i) + relative_ref = start >= 0 and argstr[start] == "<" + if relative_ref: + end = argstr.find(">", start + 1) + if end >= 0: + relative_ref = _is_relative_iri(argstr[start + 1 : end]) + + previous_length = len(res) + parsed_at = super().uri_ref2(argstr, i, res) + if parsed_at >= 0 and len(res) > previous_length and (relative_ref or relative_qname): + value = res[-1] + if isinstance(value, Node): + self.relative_iris.add(value) + return parsed_at + + +def parse_turtle_with_relative_iris(file_path: str | Path, public_id: str | None = None) -> tuple[Graph, set[Node]]: + """Parse a Turtle file and retain URI terms written as relative references.""" + + source = create_input_source(source=file_path, publicID=public_id) + graph = Graph() + try: + base_uri = graph.absolutize(source.getPublicId() or source.getSystemId() or "") + parser = _TurtleProvenanceParser(RDFSink(graph), base_uri) + stream = source.getCharacterStream() or source.getByteStream() + if stream is None: + raise ValueError("Input source does not provide a readable stream") + source_text = stream.read() + if isinstance(source_text, bytes): + source_text = source_text.decode("utf-8") + source_text = source_text.removeprefix("\ufeff") + + parser.loadStream(StringIO(source_text)) + for prefix, namespace in parser._bindings.items(): + graph.bind(prefix, namespace) + return graph, parser.relative_iris + finally: + source.close() + + +def rebase_node(node: Node, base_mappings: Iterable[tuple[str, str]]) -> Node: + """ + Return ``node`` with the first matching URI base replaced. + + Mappings must be ordered from the most specific source base to the least + specific one. Blank nodes and literals are unchanged. + """ + + if isinstance(node, URIRef): + value = str(node) + for source_base, target_base in base_mappings: + if value.startswith(source_base): + return URIRef(f"{target_base}{value[len(source_base) :]}") + + # Parsing resolves parent-relative references (for example + # ``../target``) against their source base, so the resulting URI no + # longer starts with that base. Preserve the reference's relative path + # when moving it to the prepared base. Restrict this fallback to the + # same URI authority; unrelated hosts are never affected. + value_parts = urlsplit(value) + for source_base, target_base in base_mappings: + source_parts = urlsplit(source_base) + if (value_parts.scheme, value_parts.netloc) != (source_parts.scheme, source_parts.netloc): + continue + + # A root-relative identifier is anchored at the URI authority, not + # at the crate directory. Preserve its absolute path while mapping + # the URI scheme/authority to the current crate's URI space. + if value_parts.path.startswith("/") and commonpath((source_parts.path, value_parts.path)) == "/": + target_parts = urlsplit(target_base) + return URIRef( + urlunsplit( + ( + target_parts.scheme, + target_parts.netloc, + value_parts.path, + value_parts.query, + value_parts.fragment, + ) + ) + ) + + relative_path = relpath(value_parts.path, source_parts.path) + if value_parts.path.endswith("/") and not relative_path.endswith("/"): + relative_path += "/" + rebased = urlsplit(urljoin(target_base, relative_path)) + return URIRef( + urlunsplit((rebased.scheme, rebased.netloc, rebased.path, value_parts.query, value_parts.fragment)) + ) + return node + return node + + +def rebase_graph( + graph: Graph, + base_mappings: Iterable[tuple[str, str]], + *, + preserve_nodes: Iterable[Node] = (), + rebase_objects_for: Iterable[Node] = (), + rebase_nodes: Iterable[Node] | None = None, +) -> Graph: + """ + Copy a graph while replacing selected URI bases. + + ``preserve_nodes`` keeps structural identifiers stable while their constraint + values are rebased (for example SHACL shape identifiers versus target nodes). + """ + + mappings = tuple(base_mappings) + preserved = frozenset(preserve_nodes) + rebased_object_predicates = frozenset(rebase_objects_for) + relative_nodes = None if rebase_nodes is None else frozenset(rebase_nodes) + + def transform(node: Node, *, force_rebase: bool = False) -> Node: + if relative_nodes is not None and node not in relative_nodes: + return node + return node if node in preserved and not force_rebase else rebase_node(node, mappings) + + rebased = Graph() + for prefix, namespace in graph.namespaces(): + rebased.bind(prefix, namespace) + for subject, predicate, object_ in graph: + rebased.add( + ( + transform(subject), + transform(predicate), + transform(object_, force_rebase=predicate in rebased_object_predicates), + ) + ) + return rebased + + def get_full_graph( graphs_dir: str, serialization_format: constants.RDF_SERIALIZATION_FORMATS_TYPES = "turtle", publicID: str = "." ) -> Graph: @@ -61,9 +256,13 @@ def extract_base_from_jsonld(json_data: dict) -> str | None: if isinstance(context, dict): return context.get("@base") - # If @context is a list, look for @base in each context item + # JSON-LD 1.1 Β§4.1 Context Processing Algorithm: + # https://www.w3.org/TR/json-ld11-api/#context-processing-algorithm + # Contexts are processed in order, so the final local @base takes precedence. if isinstance(context, list): - for ctx in context: + for ctx in reversed(context): + if ctx is None: + return None if isinstance(ctx, dict) and "@base" in ctx: return ctx["@base"] diff --git a/rocrate_validator/utils/uri.py b/rocrate_validator/utils/uri.py index 51e90ab9c..8bfd7b567 100644 --- a/rocrate_validator/utils/uri.py +++ b/rocrate_validator/utils/uri.py @@ -16,6 +16,7 @@ import re from pathlib import Path from urllib.parse import ParseResult, parse_qsl, urlparse, urlsplit +from urllib.request import url2pathname from rocrate_validator import errors from rocrate_validator.utils import log as logging @@ -121,6 +122,7 @@ def __init__(self, uri: str | Path): if uri is None or (isinstance(uri, str) and not uri.strip()): raise ValueError("Invalid URI: empty value") self._uri = uri = str(uri) + self._is_filesystem_path = not is_external_reference(uri) try: # Inputs that are not external references are assumed to be local # paths, so the ``file:`` scheme is added explicitly. The @@ -135,7 +137,7 @@ def __init__(self, uri: str | Path): # the authority remains a reliable signal to tell a local file # (``file:///path``) from a remote one (``file://host/path``, # RFC 8089). - if not is_external_reference(uri): + if self._is_filesystem_path: uri = f"file:{uri}" # parse the value to extract the scheme self._parse_result = urlparse(uri) @@ -152,6 +154,9 @@ def uri(self) -> str: @property def base_uri(self) -> str: + if self.is_local_resource(): + # Encode filesystem characters exactly once, without resolving symlinks. + return self.as_path().absolute().as_uri() return f"{self.scheme}://{self._parse_result.netloc}{self._parse_result.path}" @property @@ -186,7 +191,11 @@ def get_query_param(self, param: str) -> str | None: def as_path(self) -> Path: if not self.is_local_resource(): raise ValueError("URI is not a local resource") - return Path(self._uri) + # Raw paths may contain literal percent escapes. Only explicit file URIs + # are decoded; url2pathname also handles platform-specific path syntax. + if self._is_filesystem_path: + return Path(self._uri) + return Path(url2pathname(self.get_path())) def is_remote_resource(self) -> bool: """ @@ -254,7 +263,7 @@ def check_availability(self) -> AvailabilityStatus: if logger.isEnabledFor(logging.DEBUG): logger.debug(e) return AvailabilityStatus.UNAVAILABLE - return AvailabilityStatus.AVAILABLE if Path(self._uri).exists() else AvailabilityStatus.UNAVAILABLE + return AvailabilityStatus.AVAILABLE if self.as_path().exists() else AvailabilityStatus.UNAVAILABLE def is_available(self) -> bool: """ diff --git a/rocrate_validator/utils/versioning.py b/rocrate_validator/utils/versioning.py index 86e434e20..aecfee3c2 100644 --- a/rocrate_validator/utils/versioning.py +++ b/rocrate_validator/utils/versioning.py @@ -15,69 +15,78 @@ import re import subprocess import sys +from importlib import metadata +from pathlib import Path +from rocrate_validator.constants import PACKAGE_NAME from rocrate_validator.utils import log as logging -from rocrate_validator.utils.config import get_config +from rocrate_validator.utils.config import get_config, get_config_path # set up logging logger = logging.getLogger(__name__) -def run_git_command(command: list[str]) -> str | None: +def run_git_command(command: list[str], cwd: Path | None = None) -> str | None: """ Run a git command and return the output :param command: The git command + :param cwd: The directory in which to run the command :return: The output of the command """ try: - return subprocess.check_output(command, stderr=subprocess.DEVNULL).decode().strip() + return subprocess.check_output(command, cwd=cwd, stderr=subprocess.DEVNULL).decode().strip() except Exception as e: if logger.isEnabledFor(logging.DEBUG): logger.debug(e) return None -def get_git_commit() -> str: +def get_git_commit(cwd: Path | None = None) -> str: """ Get the git commit hash + :param cwd: The directory of the git repository :return: The git commit hash """ - return run_git_command(["git", "rev-parse", "--short", "HEAD"]) or "" + return run_git_command(["git", "rev-parse", "--short", "HEAD"], cwd=cwd) or "" -def is_release_tag(git_sha: str) -> bool: +def is_release_tag(git_sha: str, cwd: Path | None = None) -> bool: """ Check whether a git sha corresponds to a release tag :param git_sha: The git sha + :param cwd: The directory of the git repository :return: True if the sha corresponds to a release tag, False otherwise """ - tags = run_git_command(["git", "tag", "--points-at", git_sha]) + tags = run_git_command(["git", "tag", "--points-at", git_sha], cwd=cwd) return bool(tags) -def get_last_tag() -> str: +def get_last_tag(cwd: Path | None = None) -> str: """ Get the last tag in the git repository + :param cwd: The directory of the git repository :return: The last tag """ - return run_git_command(["git", "describe", "--tags", "--abbrev=0"]) or "" + return run_git_command(["git", "describe", "--tags", "--abbrev=0"], cwd=cwd) or "" -def get_commit_distance(tag: str | None = None) -> int: +def get_commit_distance(tag: str | None = None, cwd: Path | None = None) -> int: """ Get the distance in commits between the current commit and the last tag + :param tag: The tag from which to count + :param cwd: The directory of the git repository :return: The distance in commits """ if not tag: - tag = get_last_tag() + tag = get_last_tag(cwd=cwd) try: - count = run_git_command(["git", "rev-list", "--count", f"{tag}..HEAD"]) + count = run_git_command(["git", "rev-list", "--count", f"{tag}..HEAD"], cwd=cwd) return int(count) if count else 0 except Exception as e: if logger.isEnabledFor(logging.DEBUG): @@ -86,13 +95,23 @@ def get_commit_distance(tag: str | None = None) -> int: return 0 -def has_uncommitted_changes() -> bool: +def has_uncommitted_changes(cwd: Path | None = None) -> bool: """ Check whether there are any uncommitted changes in the repository + :param cwd: The directory of the git repository :return: True if there are uncommitted changes, False otherwise """ - return bool(run_git_command(["git", "status", "--porcelain"])) + return bool(run_git_command(["git", "status", "--porcelain"], cwd=cwd)) + + +def _is_source_checkout(package_root: Path) -> bool: + """Return whether the package files belong to their own Git checkout.""" + if not (package_root / ".git").exists(): + return False + + git_root = run_git_command(["git", "rev-parse", "--show-toplevel"], cwd=package_root) + return bool(git_root) and Path(git_root or "").resolve() == package_root.resolve() def get_version() -> str: @@ -101,20 +120,31 @@ def get_version() -> str: :return: The version """ - version = None + package_root = get_config_path().parent + source_checkout = _is_source_checkout(package_root) + + if not source_checkout: + try: + return metadata.version(PACKAGE_NAME) + except metadata.PackageNotFoundError: + pass + config = get_config() declared_version = config["tool"]["poetry"]["version"] - commit_sha = get_git_commit() - is_release = is_release_tag(commit_sha) - latest_tag = get_last_tag() + if not source_checkout: + return declared_version + + commit_sha = get_git_commit(cwd=package_root) + is_release = is_release_tag(commit_sha, cwd=package_root) + latest_tag = get_last_tag(cwd=package_root) if is_release: if declared_version != latest_tag: logger.warning("The declared version %s is different from the last tag %s", declared_version, latest_tag) version = latest_tag else: - commit_distance = get_commit_distance(latest_tag) + commit_distance = get_commit_distance(latest_tag, cwd=package_root) version = f"{declared_version}_{commit_sha}+{commit_distance}" if commit_sha else declared_version - dirty = has_uncommitted_changes() + dirty = has_uncommitted_changes(cwd=package_root) return f"{version}-dirty" if dirty else version diff --git a/tests/conftest.py b/tests/conftest.py index 4f4ad1e36..883b24a20 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,8 +39,11 @@ # profiles paths PROFILES_PATH = str(Path(f"{CURRENT_PATH}/../rocrate_validator/profiles").resolve()) -# Dynamically update the SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER -rocrate_profile = services.get_profile("ro-crate") +# Dynamically update the SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER. +# Pinned to `ro-crate-1.1` on purpose: the suites using this identifier validate +# against 1.1 or against profiles that inherit from it, so the check id has to +# come from 1.1 regardless of which profile is the default. +rocrate_profile = services.get_profile("ro-crate-1.1") if not rocrate_profile: raise RuntimeError("Unable to load the RO-Crate profile") check_local_data_entity_existence = rocrate_profile.get_requirement_check("Data Entity: REQUIRED resource availability") diff --git a/tests/data/crates/invalid/4_data_entity_metadata/valid_encoding_format_ctx_entity/ro-crate-metadata.json b/tests/data/crates/invalid/4_data_entity_metadata/valid_encoding_format_ctx_entity/ro-crate-metadata.json index c68db6acb..7a38071ea 100644 --- a/tests/data/crates/invalid/4_data_entity_metadata/valid_encoding_format_ctx_entity/ro-crate-metadata.json +++ b/tests/data/crates/invalid/4_data_entity_metadata/valid_encoding_format_ctx_entity/ro-crate-metadata.json @@ -21,11 +21,9 @@ "mainEntity": { "@id": "sort-and-change-case.ga" }, - "license": [ - { - "@id": "https://spdx.org/licenses/MIT.html" - } - ], + "license": { + "@id": "https://spdx.org/licenses/MIT.html" + }, "author": { "@id": "https://orcid.org/0000-0002-1825-0097" }, @@ -82,7 +80,7 @@ "subjectOf": { "@id": "sort-and-change-case.cwl" }, - "encodingFormat": ["application/galaxy"] + "encodingFormat": "application/galaxy" }, { "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", @@ -94,16 +92,14 @@ "url": { "@id": "https://galaxyproject.org/" }, - "encodingFormat": ["application/galaxy"] + "encodingFormat": "application/galaxy" }, { "@id": "blank.png", "@type": ["File", "ImageObject"], - "encodingFormat": [ - { - "@id": "https://www.iana.org/assignments/media-types/image/png" - } - ] + "encodingFormat": { + "@id": "https://www.iana.org/assignments/media-types/image/png" + } }, { "@id": "https://www.iana.org/assignments/media-types/image/png", @@ -116,7 +112,7 @@ "about": { "@id": "./" }, - "encodingFormat": ["text/markdown"] + "encodingFormat": "text/markdown" }, { "@id": "https://spdx.org/licenses/MIT.html", "@type": "CreativeWork" } ] diff --git a/tests/data/crates/invalid/4_data_entity_metadata/valid_encoding_format_pronom/ro-crate-metadata.json b/tests/data/crates/invalid/4_data_entity_metadata/valid_encoding_format_pronom/ro-crate-metadata.json index 3d93deecb..f996dcd79 100644 --- a/tests/data/crates/invalid/4_data_entity_metadata/valid_encoding_format_pronom/ro-crate-metadata.json +++ b/tests/data/crates/invalid/4_data_entity_metadata/valid_encoding_format_pronom/ro-crate-metadata.json @@ -21,11 +21,9 @@ "mainEntity": { "@id": "sort-and-change-case.ga" }, - "license": [ - { - "@id": "https://spdx.org/licenses/MIT.html" - } - ], + "license": { + "@id": "https://spdx.org/licenses/MIT.html" + }, "author": { "@id": "https://orcid.org/0000-0002-1825-0097" }, @@ -82,7 +80,7 @@ "subjectOf": { "@id": "sort-and-change-case.cwl" }, - "encodingFormat": ["application/galaxy"] + "encodingFormat": "application/galaxy" }, { "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", @@ -94,12 +92,12 @@ "url": { "@id": "https://galaxyproject.org/" }, - "encodingFormat": ["application/galaxy"] + "encodingFormat": "application/galaxy" }, { "@id": "blank.png", "@type": ["File", "ImageObject"], - "encodingFormat": ["image/png"] + "encodingFormat": "image/png" }, { "@id": "README.md", @@ -107,7 +105,7 @@ "about": { "@id": "./" }, - "encodingFormat": ["text/markdown"] + "encodingFormat": "text/markdown" }, { "@id": "https://spdx.org/licenses/MIT.html", "@type": "CreativeWork" } ] diff --git a/tests/data/crates/invalid/ro-crate-1.3/detached-bad-filename/ro-crate-metadata.json b/tests/data/crates/invalid/ro-crate-1.3/detached-bad-filename/ro-crate-metadata.json new file mode 100644 index 000000000..8b72c5c26 --- /dev/null +++ b/tests/data/crates/invalid/ro-crate-1.3/detached-bad-filename/ro-crate-metadata.json @@ -0,0 +1,37 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://example.org/ro-crate/detached" + } + }, + { + "@id": "https://example.org/ro-crate/detached", + "@type": "Dataset", + "name": "Detached RO-Crate filename", + "description": "Detached RO-Crate with discouraged filename.", + "license": { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/" + }, + "datePublished": "2024-01-01", + "hasPart": [ + { + "@id": "https://example.org/data/file1.txt" + } + ] + }, + { + "@id": "https://example.org/data/file1.txt", + "@type": "File", + "name": "file1.txt", + "description": "Remote file in detached RO-Crate.", + "encodingFormat": "text/plain" + } + ] +} diff --git a/tests/data/crates/invalid/ro-crate-1.3/detached-relative-entity/dataset-ro-crate-metadata.json b/tests/data/crates/invalid/ro-crate-1.3/detached-relative-entity/dataset-ro-crate-metadata.json new file mode 100644 index 000000000..77554ef38 --- /dev/null +++ b/tests/data/crates/invalid/ro-crate-1.3/detached-relative-entity/dataset-ro-crate-metadata.json @@ -0,0 +1,37 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://example.org/ro-crate/detached" + } + }, + { + "@id": "https://example.org/ro-crate/detached", + "@type": "Dataset", + "name": "Detached RO-Crate invalid", + "description": "Detached crate with relative data entity.", + "license": { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/" + }, + "datePublished": "2024-01-01", + "hasPart": [ + { + "@id": "data.txt" + } + ] + }, + { + "@id": "data.txt", + "@type": "File", + "name": "data.txt", + "description": "Relative file in detached RO-Crate.", + "encodingFormat": "text/plain" + } + ] +} diff --git a/tests/data/crates/invalid/ro-crate-1.3/invalid-context/data.txt b/tests/data/crates/invalid/ro-crate-1.3/invalid-context/data.txt new file mode 100644 index 000000000..ce0136250 --- /dev/null +++ b/tests/data/crates/invalid/ro-crate-1.3/invalid-context/data.txt @@ -0,0 +1 @@ +hello diff --git a/tests/data/crates/invalid/ro-crate-1.3/invalid-context/ro-crate-metadata.json b/tests/data/crates/invalid/ro-crate-1.3/invalid-context/ro-crate-metadata.json new file mode 100644 index 000000000..96387ffdd --- /dev/null +++ b/tests/data/crates/invalid/ro-crate-1.3/invalid-context/ro-crate-metadata.json @@ -0,0 +1,37 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Invalid context", + "description": "Uses 1.1 context.", + "license": { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/" + }, + "datePublished": "2024-01-01", + "hasPart": [ + { + "@id": "data.txt" + } + ] + }, + { + "@id": "data.txt", + "@type": "File", + "name": "data.txt", + "description": "Example data file.", + "encodingFormat": "text/plain" + } + ] +} diff --git a/tests/data/crates/invalid/ro-crate-1.3/invalid-date-published/data.txt b/tests/data/crates/invalid/ro-crate-1.3/invalid-date-published/data.txt new file mode 100644 index 000000000..ce0136250 --- /dev/null +++ b/tests/data/crates/invalid/ro-crate-1.3/invalid-date-published/data.txt @@ -0,0 +1 @@ +hello diff --git a/tests/data/crates/invalid/ro-crate-1.3/invalid-date-published/ro-crate-metadata.json b/tests/data/crates/invalid/ro-crate-1.3/invalid-date-published/ro-crate-metadata.json new file mode 100644 index 000000000..69cdf6b37 --- /dev/null +++ b/tests/data/crates/invalid/ro-crate-1.3/invalid-date-published/ro-crate-metadata.json @@ -0,0 +1,37 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Invalid datePublished", + "description": "datePublished is an array.", + "license": { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/" + }, + "datePublished": ["2024-01-01", "2024-01-02"], + "hasPart": [ + { + "@id": "data.txt" + } + ] + }, + { + "@id": "data.txt", + "@type": "File", + "name": "data.txt", + "description": "Example data file.", + "encodingFormat": "text/plain" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/bad_input/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/bad_input/ro-crate-metadata.json new file mode 100644 index 000000000..f48351927 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/bad_input/ro-crate-metadata.json @@ -0,0 +1,188 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow", + "HowTo" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "input": [ + { + "@id": "#input-param" + } + ], + "output": [ + { + "@id": "#output-param" + } + ], + "step": [ + { + "@id": "#sort" + }, + { + "@id": "#change-case" + } + ], + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "#input-param", + "@type": "CreativeWork" + }, + { + "@id": "#output-param", + "@type": "FormalParameter", + "additionalType": "File", + "name": "output", + "description": "output text file" + }, + { + "@id": "#sort", + "@type": "HowToStep", + "position": "0" + }, + { + "@id": "#change-case", + "@type": "HowToStep", + "position": "1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/bad_output/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/bad_output/ro-crate-metadata.json new file mode 100644 index 000000000..c6e489e5f --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/bad_output/ro-crate-metadata.json @@ -0,0 +1,188 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow", + "HowTo" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "input": [ + { + "@id": "#input-param" + } + ], + "output": [ + { + "@id": "#output-param" + } + ], + "step": [ + { + "@id": "#sort" + }, + { + "@id": "#change-case" + } + ], + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "#input-param", + "@type": "FormalParameter", + "additionalType": "File", + "name": "input", + "description": "input text file" + }, + { + "@id": "#output-param", + "@type": "CreativeWork" + }, + { + "@id": "#sort", + "@type": "HowToStep", + "position": "0" + }, + { + "@id": "#change-case", + "@type": "HowToStep", + "position": "1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/no_input/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/no_input/ro-crate-metadata.json new file mode 100644 index 000000000..b08c3c3f6 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/no_input/ro-crate-metadata.json @@ -0,0 +1,179 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow", + "HowTo" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "output": [ + { + "@id": "#output-param" + } + ], + "step": [ + { + "@id": "#sort" + }, + { + "@id": "#change-case" + } + ], + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "#output-param", + "@type": "FormalParameter", + "additionalType": "File", + "name": "output", + "description": "output text file" + }, + { + "@id": "#sort", + "@type": "HowToStep", + "position": "0" + }, + { + "@id": "#change-case", + "@type": "HowToStep", + "position": "1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/no_output/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/no_output/ro-crate-metadata.json new file mode 100644 index 000000000..0e5aed89e --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/computational_workflow/no_output/ro-crate-metadata.json @@ -0,0 +1,179 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow", + "HowTo" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "input": [ + { + "@id": "#input-param" + } + ], + "step": [ + { + "@id": "#sort" + }, + { + "@id": "#change-case" + } + ], + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "#input-param", + "@type": "FormalParameter", + "additionalType": "File", + "name": "input", + "description": "input text file" + }, + { + "@id": "#sort", + "@type": "HowToStep", + "position": "0" + }, + { + "@id": "#change-case", + "@type": "HowToStep", + "position": "1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/howtostep/howtostep_no_position/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/howtostep/howtostep_no_position/ro-crate-metadata.json new file mode 100644 index 000000000..084777d19 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/howtostep/howtostep_no_position/ro-crate-metadata.json @@ -0,0 +1,166 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow", + "HowTo" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "step": [ + { + "@id": "#sort" + }, + { + "@id": "#change-case" + } + ], + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "#sort", + "@type": "HowToStep" + }, + { + "@id": "#change-case", + "@type": "HowToStep", + "position": "1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_bad_conformsto/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_bad_conformsto/ro-crate-metadata.json new file mode 100644 index 000000000..ce423399e --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_bad_conformsto/ro-crate-metadata.json @@ -0,0 +1,148 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/0.5-DRAFT-2020_07_21" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_bad_step/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_bad_step/ro-crate-metadata.json new file mode 100644 index 000000000..eba7c481e --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_bad_step/ro-crate-metadata.json @@ -0,0 +1,190 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow", + "HowTo" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "input": [ + { + "@id": "#input-param" + } + ], + "output": [ + { + "@id": "#output-param" + } + ], + "step": [ + { + "@id": "#sort" + }, + { + "@id": "#change-case" + } + ], + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "#input-param", + "@type": "FormalParameter", + "additionalType": "File", + "name": "input", + "description": "input text file" + }, + { + "@id": "#output-param", + "@type": "FormalParameter", + "additionalType": "File", + "name": "output", + "description": "output text file" + }, + { + "@id": "#sort", + "@type": "CreativeWork" + }, + { + "@id": "#change-case", + "@type": "HowToStep", + "position": "1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_bad_type/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_bad_type/ro-crate-metadata.json new file mode 100644 index 000000000..0c5f6df26 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_bad_type/ro-crate-metadata.json @@ -0,0 +1,148 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "Action", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_cwl_desc_bad_type/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_cwl_desc_bad_type/ro-crate-metadata.json new file mode 100644 index 000000000..484fad7dd --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_cwl_desc_bad_type/ro-crate-metadata.json @@ -0,0 +1,147 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_cwl_desc_no_lang/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_cwl_desc_no_lang/ro-crate-metadata.json new file mode 100644 index 000000000..e1775cd15 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_cwl_desc_no_lang/ro-crate-metadata.json @@ -0,0 +1,132 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_cwl_desc/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_cwl_desc/ro-crate-metadata.json new file mode 100644 index 000000000..21a6e7841 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_cwl_desc/ro-crate-metadata.json @@ -0,0 +1,130 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_file/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_file/ro-crate-metadata.json new file mode 100644 index 000000000..8ce38ec98 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_file/ro-crate-metadata.json @@ -0,0 +1,148 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_image/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_image/ro-crate-metadata.json new file mode 100644 index 000000000..7846f1605 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_image/ro-crate-metadata.json @@ -0,0 +1,135 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_lang/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_lang/ro-crate-metadata.json new file mode 100644 index 000000000..3f6b31033 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_lang/ro-crate-metadata.json @@ -0,0 +1,133 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_step/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_step/ro-crate-metadata.json new file mode 100644 index 000000000..1318e62d5 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_no_step/ro-crate-metadata.json @@ -0,0 +1,173 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow", + "HowTo" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "input": [ + { + "@id": "#input-param" + } + ], + "output": [ + { + "@id": "#output-param" + } + ], + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "#input-param", + "@type": "FormalParameter", + "additionalType": "File", + "name": "input", + "description": "input text file" + }, + { + "@id": "#output-param", + "@type": "FormalParameter", + "additionalType": "File", + "name": "output", + "description": "output text file" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_with_steps_bad_type/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_with_steps_bad_type/ro-crate-metadata.json new file mode 100644 index 000000000..1dfd8b744 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/main_workflow/main_workflow_with_steps_bad_type/ro-crate-metadata.json @@ -0,0 +1,166 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "step": [ + { + "@id": "#sort" + }, + { + "@id": "#change-case" + } + ], + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "#sort", + "@type": "HowToStep", + "position": "0" + }, + { + "@id": "#change-case", + "@type": "HowToStep", + "position": "1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/bad_profile/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/bad_profile/ro-crate-metadata.json new file mode 100644 index 000000000..b2235ae72 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/bad_profile/ro-crate-metadata.json @@ -0,0 +1,148 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/foo/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/foo/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/no_license/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/no_license/ro-crate-metadata.json new file mode 100644 index 000000000..0420444e0 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/no_license/ro-crate-metadata.json @@ -0,0 +1,126 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + } + ], + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/no_mainentity/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/no_mainentity/ro-crate-metadata.json new file mode 100644 index 000000000..51917090e --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/no_mainentity/ro-crate-metadata.json @@ -0,0 +1,145 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/readme_not_about_crate/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/readme_not_about_crate/ro-crate-metadata.json new file mode 100644 index 000000000..810c58a7d --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/readme_not_about_crate/ro-crate-metadata.json @@ -0,0 +1,145 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/readme_wrong_encoding_format/ro-crate-metadata.json b/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/readme_wrong_encoding_format/ro-crate-metadata.json new file mode 100644 index 000000000..4f73b3850 --- /dev/null +++ b/tests/data/crates/invalid/workflow-ro-crate-1.1/wroc_crate/readme_wrong_encoding_format/ro-crate-metadata.json @@ -0,0 +1,148 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/csv" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/rocrate-1.2/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json index 1ffe2309e..17083cce4 100644 --- a/tests/data/crates/rocrate-1.2/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json @@ -26,11 +26,9 @@ "funder": { "@id": "https://ror.org/05f9q8d28" }, - "hasPart": [ - { - "@id": "script.sh" - } - ], + "hasPart": { + "@id": "script.sh" + }, "identifier": { "@id": "#uuid" } diff --git a/tests/data/crates/rocrate-1.2/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json index af4cfbf1b..79d612ff4 100644 --- a/tests/data/crates/rocrate-1.2/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json @@ -29,11 +29,9 @@ "conformsTo": { "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" }, - "hasPart": [ - { - "@id": "workflow.ga" - } - ], + "hasPart": { + "@id": "workflow.ga" + }, "identifier": { "@id": "#uuid" } diff --git a/tests/data/crates/rocrate-1.2/1_metadata_document/described_contextual_entities/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/1_metadata_document/described_contextual_entities/valid/ro-crate-metadata.json index 362c81473..fd9b4fa9d 100644 --- a/tests/data/crates/rocrate-1.2/1_metadata_document/described_contextual_entities/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/1_metadata_document/described_contextual_entities/valid/ro-crate-metadata.json @@ -26,11 +26,9 @@ "publisher": { "@id": "https://ror.org/05f9q8d28" }, - "hasPart": [ - { - "@id": "data.csv" - } - ], + "hasPart": { + "@id": "data.csv" + }, "funder": { "@id": "https://ror.org/05f9q8d28" }, diff --git a/tests/data/crates/rocrate-1.2/1_metadata_document/referenced_contextual_entities/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/1_metadata_document/referenced_contextual_entities/valid/ro-crate-metadata.json index 362c81473..fd9b4fa9d 100644 --- a/tests/data/crates/rocrate-1.2/1_metadata_document/referenced_contextual_entities/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/1_metadata_document/referenced_contextual_entities/valid/ro-crate-metadata.json @@ -26,11 +26,9 @@ "publisher": { "@id": "https://ror.org/05f9q8d28" }, - "hasPart": [ - { - "@id": "data.csv" - } - ], + "hasPart": { + "@id": "data.csv" + }, "funder": { "@id": "https://ror.org/05f9q8d28" }, diff --git a/tests/data/crates/rocrate-1.2/1_metadata_document/schema_org_iri_protocol/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/1_metadata_document/schema_org_iri_protocol/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..3f3f682f5 --- /dev/null +++ b/tests/data/crates/rocrate-1.2/1_metadata_document/schema_org_iri_protocol/invalid/ro-crate-metadata.json @@ -0,0 +1,65 @@ +{ + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.2" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Schema.org IRI protocol β€” invalid", + "description": "RO-Crate that types an entity with an https Schema.org IRI and references Schema.org terms through https @id values.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": [], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": { + "@id": "https://schema.org/identifier" + }, + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "https://schema.org/Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "additionalType": { + "@id": "https://schema.org/ResearchOrganization" + }, + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.2/1_metadata_document/schema_org_iri_protocol/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/1_metadata_document/schema_org_iri_protocol/valid/ro-crate-metadata.json new file mode 100644 index 000000000..a1dec1a8d --- /dev/null +++ b/tests/data/crates/rocrate-1.2/1_metadata_document/schema_org_iri_protocol/valid/ro-crate-metadata.json @@ -0,0 +1,63 @@ +{ + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.2" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Schema.org IRI protocol β€” valid", + "description": "RO-Crate whose Schema.org IRIs all use the http namespace that the RO-Crate @context defines.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": [], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "additionalType": { + "@id": "http://schema.org/ResearchOrganization" + }, + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.2/2_attached_rocrates/attached-preview-not-in-hasPart/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/2_attached_rocrates/attached-preview-not-in-hasPart/valid/ro-crate-metadata.json index b3c407993..c04c0bff3 100644 --- a/tests/data/crates/rocrate-1.2/2_attached_rocrates/attached-preview-not-in-hasPart/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/2_attached_rocrates/attached-preview-not-in-hasPart/valid/ro-crate-metadata.json @@ -23,11 +23,9 @@ "publisher": { "@id": "https://ror.org/00k4n6c32" }, - "hasPart": [ - { - "@id": "data.csv" - } - ], + "hasPart": { + "@id": "data.csv" + }, "funder": { "@id": "https://ror.org/00k4n6c32" }, diff --git a/tests/data/crates/rocrate-1.2/5_metadata_entities/preprocessing_candidates/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/5_metadata_entities/preprocessing_candidates/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..c321cda85 --- /dev/null +++ b/tests/data/crates/rocrate-1.2/5_metadata_entities/preprocessing_candidates/invalid/ro-crate-metadata.json @@ -0,0 +1,113 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.2/context", + { + "propertyUrl": "http://www.w3.org/ns/csvw#propertyUrl" + } + ], + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.2" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Invalid preprocessing candidate fixture", + "description": "A crate with claimed and type-only entities missing required metadata.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0009-0000-5074-6239" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": { + "@id": "https://vocab.example.org/identifier-scheme" + }, + "inDefinedTermSet": { + "@id": "https://vocab.example.org/identifier-vocabulary" + }, + "encodingFormat": { + "@id": "https://vocab.example.org/formats/uuid" + }, + "conformsTo": { + "@id": "https://vocab.example.org/profiles/property-value" + }, + "propertyUrl": { + "@id": "https://vocab.example.org/properties/uuid" + }, + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://orcid.org/0009-0000-5074-6239", + "@type": "Person", + "affiliation": { + "@id": "https://ror.org/05f9q8d28" + }, + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "#type-only-person", + "@type": "Person" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org/research", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "Example Funding Agency", + "url": "https://example.org/funding", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Example contact", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Funding contact", + "email": "mailto:funding@example.org" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon the work." + } + ] +} diff --git a/tests/data/crates/rocrate-1.2/5_metadata_entities/preprocessing_candidates/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/5_metadata_entities/preprocessing_candidates/valid/ro-crate-metadata.json new file mode 100644 index 000000000..18fb33168 --- /dev/null +++ b/tests/data/crates/rocrate-1.2/5_metadata_entities/preprocessing_candidates/valid/ro-crate-metadata.json @@ -0,0 +1,124 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.2/context", + { + "customPredicate": "https://vocab.example.org/property/customPredicate", + "propertyUrl": "http://www.w3.org/ns/csvw#propertyUrl" + } + ], + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.2" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Preprocessing candidate fixture", + "description": "A valid crate covering cited-only and claim-referenced identifiers.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0009-0000-5074-6239" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "customPredicate": { + "@id": "https://vocab.example.org/entities/described" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": { + "@id": "https://vocab.example.org/identifier-scheme" + }, + "inDefinedTermSet": { + "@id": "https://vocab.example.org/identifier-vocabulary" + }, + "encodingFormat": { + "@id": "https://vocab.example.org/formats/uuid" + }, + "conformsTo": { + "@id": "https://vocab.example.org/profiles/property-value" + }, + "propertyUrl": { + "@id": "https://vocab.example.org/properties/uuid" + }, + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://vocab.example.org/entities/described", + "@type": [ + "Thing", + "https://vocab.example.org/types/CustomEntity" + ], + "name": "Described vocabulary entity", + "description": "This entity belongs to an unlisted namespace but is described by the crate.", + "customPredicate": "custom value" + }, + { + "@id": "https://orcid.org/0009-0000-5074-6239", + "@type": "Person", + "name": "Example Author", + "affiliation": { + "@id": "https://ror.org/05f9q8d28" + }, + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org/research", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "Example Funding Agency", + "url": "https://example.org/funding", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Example contact", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Funding contact", + "email": "mailto:funding@example.org" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon the work." + } + ] +} diff --git a/tests/data/crates/rocrate-1.2/5_metadata_entities/recommended_name/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/5_metadata_entities/recommended_name/valid/ro-crate-metadata.json index 92642673d..17ea52215 100644 --- a/tests/data/crates/rocrate-1.2/5_metadata_entities/recommended_name/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/5_metadata_entities/recommended_name/valid/ro-crate-metadata.json @@ -20,11 +20,9 @@ "license": { "@id": "https://creativecommons.org/licenses/by/4.0/" }, - "hasPart": [ - { - "@id": "my-data-file.txt" - } - ], + "hasPart": { + "@id": "my-data-file.txt" + }, "publisher": { "@id": "https://ror.org/05f9q8d28" }, diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/data_entity_license/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/data_entity_license/invalid/ro-crate-metadata.json index 166a1654e..bb253fcf5 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/data_entity_license/invalid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/data_entity_license/invalid/ro-crate-metadata.json @@ -23,11 +23,9 @@ "publisher": { "@id": "https://ror.org/012345678" }, - "hasPart": [ - { - "@id": "data/research-data.csv" - } - ], + "hasPart": { + "@id": "data/research-data.csv" + }, "funder": { "@id": "https://ror.org/012345678" }, diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/data_entity_license/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/data_entity_license/valid/ro-crate-metadata.json index 97bff43db..94f936143 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/data_entity_license/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/data_entity_license/valid/ro-crate-metadata.json @@ -23,11 +23,9 @@ "publisher": { "@id": "https://ror.org/012345678" }, - "hasPart": [ - { - "@id": "data/open-data.csv" - } - ], + "hasPart": { + "@id": "data/open-data.csv" + }, "funder": { "@id": "https://ror.org/012345678" }, diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/dataset_has_part/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/dataset_has_part/valid/ro-crate-metadata.json index 421954310..264d7d103 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/dataset_has_part/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/dataset_has_part/valid/ro-crate-metadata.json @@ -29,11 +29,9 @@ "identifier": { "@id": "#uuid" }, - "hasPart": [ - { - "@id": "subdir/" - } - ] + "hasPart": { + "@id": "subdir/" + } }, { "@id": "#uuid", @@ -47,11 +45,9 @@ "@type": "Dataset", "name": "Sub-directory with hasPart", "description": "A local Dataset sub-entity that correctly lists its contents via hasPart.", - "hasPart": [ - { - "@id": "subdir/data.csv" - } - ] + "hasPart": { + "@id": "subdir/data.csv" + } }, { "@id": "subdir/data.csv", diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/dataset_trailing_slash/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/dataset_trailing_slash/valid/ro-crate-metadata.json index c0d6b3128..479439205 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/dataset_trailing_slash/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/dataset_trailing_slash/valid/ro-crate-metadata.json @@ -29,11 +29,9 @@ "identifier": { "@id": "#uuid" }, - "hasPart": [ - { - "@id": "subdir/" - } - ] + "hasPart": { + "@id": "subdir/" + } }, { "@id": "#uuid", @@ -47,11 +45,9 @@ "@type": "Dataset", "name": "Sub-directory", "description": "A local Dataset sub-entity whose @id correctly ends with '/'.", - "hasPart": [ - { - "@id": "subdir/data.csv" - } - ] + "hasPart": { + "@id": "subdir/data.csv" + } }, { "@id": "subdir/data.csv", diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/local_entity_reference/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/local_entity_reference/invalid/ro-crate-metadata.json index 23199ec88..088c7b8ba 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/local_entity_reference/invalid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/local_entity_reference/invalid/ro-crate-metadata.json @@ -22,12 +22,12 @@ }, "hasPart": [ { - "@id": "file://./my-test-file.txt" + "@id": "/my-test-file.txt" } ] }, { - "@id": "file://./my-test-file.txt", + "@id": "/my-test-file.txt", "@type": "File", "name": "My Test File", "description": "A test file for RO-Crate.", diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_conformsto/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_conformsto/valid/ro-crate-metadata.json index 44d0718e9..23010f2dc 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_conformsto/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_conformsto/valid/ro-crate-metadata.json @@ -26,11 +26,9 @@ "funder": { "@id": "https://ror.org/00k4n6c32" }, - "hasPart": [ - { - "@id": "data.csv" - } - ], + "hasPart": { + "@id": "data.csv" + }, "identifier": { "@id": "#uuid" } diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_contentSize/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_contentSize/valid/ro-crate-metadata.json index 73d8d4ba9..b0999ab6e 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_contentSize/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_contentSize/valid/ro-crate-metadata.json @@ -26,11 +26,9 @@ "funder": { "@id": "https://ror.org/00k4n6c32" }, - "hasPart": [ - { - "@id": "data.csv" - } - ], + "hasPart": { + "@id": "data.csv" + }, "identifier": { "@id": "#uuid" } diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_encoding_format/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_encoding_format/valid/ro-crate-metadata.json index 364e3b786..8c53db315 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_encoding_format/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_encoding_format/valid/ro-crate-metadata.json @@ -26,11 +26,9 @@ "publisher": { "@id": "https://ror.org/05f9q8d28" }, - "hasPart": [ - { - "@id": "data.csv" - } - ], + "hasPart": { + "@id": "data.csv" + }, "funder": { "@id": "https://ror.org/05f9q8d28" }, diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_properties/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_properties/valid/ro-crate-metadata.json index 364e3b786..8c53db315 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_properties/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_properties/valid/ro-crate-metadata.json @@ -26,11 +26,9 @@ "publisher": { "@id": "https://ror.org/05f9q8d28" }, - "hasPart": [ - { - "@id": "data.csv" - } - ], + "hasPart": { + "@id": "data.csv" + }, "funder": { "@id": "https://ror.org/05f9q8d28" }, diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_sdDatePublished/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_sdDatePublished/valid/ro-crate-metadata.json index 8fc1776f2..a388d9fc5 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_sdDatePublished/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/recommended_sdDatePublished/valid/ro-crate-metadata.json @@ -26,11 +26,9 @@ "funder": { "@id": "https://ror.org/00k4n6c32" }, - "hasPart": [ - { - "@id": "https://example.com/data.csv" - } - ], + "hasPart": { + "@id": "https://example.com/data.csv" + }, "identifier": { "@id": "#uuid" } diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_directory_distribution/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_directory_distribution/valid/ro-crate-metadata.json index e099665cb..7083be4a1 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_directory_distribution/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_directory_distribution/valid/ro-crate-metadata.json @@ -23,11 +23,9 @@ "publisher": { "@id": "https://ror.org/05f9q8d28" }, - "hasPart": [ - { - "@id": "https://example.com/dataset-dir/" - } - ], + "hasPart": { + "@id": "https://example.com/dataset-dir/" + }, "funder": { "@id": "https://ror.org/00k4n6c32" }, diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_entity_content_url/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_entity_content_url/valid/ro-crate-metadata.json index 01ad62a9e..ff2c10d0d 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_entity_content_url/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_entity_content_url/valid/ro-crate-metadata.json @@ -23,11 +23,9 @@ "publisher": { "@id": "https://ror.org/05f9q8d28" }, - "hasPart": [ - { - "@id": "https://example.com/landing/dataset" - } - ], + "hasPart": { + "@id": "https://example.com/landing/dataset" + }, "funder": { "@id": "https://ror.org/00k4n6c32" }, diff --git a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_entity_downloadable/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_entity_downloadable/valid/ro-crate-metadata.json index 564d070b0..4ed18e3bb 100644 --- a/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_entity_downloadable/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/8_metadata_dataEntities/web_entity_downloadable/valid/ro-crate-metadata.json @@ -23,11 +23,9 @@ "publisher": { "@id": "https://ror.org/05f9q8d28" }, - "hasPart": [ - { - "@id": "https://example.com/data.csv" - } - ], + "hasPart": { + "@id": "https://example.com/data.csv" + }, "funder": { "@id": "https://ror.org/00k4n6c32" }, diff --git a/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid/ro-crate-metadata.json index 7d605e478..fd8ad5c4b 100644 --- a/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid/ro-crate-metadata.json @@ -30,11 +30,9 @@ "funder": { "@id": "https://ror.org/012345678" }, - "hasPart": [ - { - "@id": "https://example.org/other-ro-crate/" - } - ], + "hasPart": { + "@id": "https://example.org/other-ro-crate/" + }, "identifier": { "@id": "#uuid" } diff --git a/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid_with_identifier/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid_with_identifier/ro-crate-metadata.json index 512cdfd0d..6936caf75 100644 --- a/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid_with_identifier/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid_with_identifier/ro-crate-metadata.json @@ -30,11 +30,9 @@ "funder": { "@id": "https://ror.org/012345678" }, - "hasPart": [ - { - "@id": "https://example.org/persistent-crate/" - } - ], + "hasPart": { + "@id": "https://example.org/persistent-crate/" + }, "identifier": { "@id": "#uuid" } diff --git a/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid_with_relative_path/ro-crate-metadata.json b/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid_with_relative_path/ro-crate-metadata.json index 88e6b56b0..714f2e75d 100644 --- a/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid_with_relative_path/ro-crate-metadata.json +++ b/tests/data/crates/rocrate-1.2/9_referenced_rocrate/valid_with_relative_path/ro-crate-metadata.json @@ -30,11 +30,9 @@ "funder": { "@id": "https://ror.org/012345678" }, - "hasPart": [ - { - "@id": "subcrate/" - } - ], + "hasPart": { + "@id": "subcrate/" + }, "identifier": { "@id": "#uuid" } @@ -59,11 +57,9 @@ "@id": "https://w3id.org/ro/crate/1.2" } ], - "hasPart": [ - { - "@id": "subcrate/data.txt" - } - ], + "hasPart": { + "@id": "subcrate/data.txt" + }, "subjectOf": { "@id": "#subcrate-metadata" } diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/contextual_entity_id_format/invalid_bare_contactpoint/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/contextual_entity_id_format/invalid_bare_contactpoint/ro-crate-metadata.json new file mode 100644 index 000000000..4e2558a72 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/contextual_entity_id_format/invalid_bare_contactpoint/ro-crate-metadata.json @@ -0,0 +1,47 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Contextual entity @id format β€” invalid (bare ContactPoint id)", + "description": "RO-Crate where a ContactPoint contextual entity has a bare relative @id (no '#' prefix, not an absolute URI).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0000-0001-0000-0001" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "https://orcid.org/0000-0001-0000-0001", + "@type": "Person", + "name": "Alice Researcher", + "contactPoint": { + "@id": "alice-contact" + } + }, + { + "@id": "alice-contact", + "@type": "ContactPoint", + "name": "Alice's contact point", + "email": "alice@example.edu" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/contextual_entity_id_format/invalid_bare_propertyvalue/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/contextual_entity_id_format/invalid_bare_propertyvalue/ro-crate-metadata.json new file mode 100644 index 000000000..d8c267e93 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/contextual_entity_id_format/invalid_bare_propertyvalue/ro-crate-metadata.json @@ -0,0 +1,40 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Contextual entity @id format β€” invalid (bare PropertyValue id)", + "description": "RO-Crate where a PropertyValue contextual entity has a bare relative @id (no '#' prefix, not an absolute URI).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "identifier": { + "@id": "uuid-property" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "uuid-property", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/contextual_entity_id_format/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/contextual_entity_id_format/valid/ro-crate-metadata.json new file mode 100644 index 000000000..f6bb4929e --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/contextual_entity_id_format/valid/ro-crate-metadata.json @@ -0,0 +1,91 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Contextual entity @id format β€” valid", + "description": "RO-Crate whose contextual entities all use '#'-prefixed local identifiers or absolute URI permalinks.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0000-0001-0000-0001" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "identifier": { + "@id": "#uuid" + }, + "contentLocation": { + "@id": "#lab-place" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0", + "description": "Creative Commons Attribution 4.0 International license." + }, + { + "@id": "https://orcid.org/0000-0001-0000-0001", + "@type": "Person", + "name": "Alice Researcher", + "affiliation": { + "@id": "https://ror.org/05f9q8d28" + }, + "contactPoint": { + "@id": "#alice-contact" + } + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example University", + "url": "https://example.edu", + "contactPoint": { + "@id": "#org-contact" + } + }, + { + "@id": "#alice-contact", + "@type": "ContactPoint", + "name": "Alice's contact point", + "email": "alice@example.edu" + }, + { + "@id": "#org-contact", + "@type": "ContactPoint", + "name": "Example University contact", + "email": "contact@example.edu" + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "#lab-place", + "@type": "Place", + "name": "Example Research Lab", + "description": "The physical location where the research took place." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/encoding_format/info_no_webpageelement/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/encoding_format/info_no_webpageelement/ro-crate-metadata.json new file mode 100644 index 000000000..6cbc836ca --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/encoding_format/info_no_webpageelement/ro-crate-metadata.json @@ -0,0 +1,55 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Encoding format entity β€” info (no WebPageElement)", + "description": "RO-Crate whose encoding format entity has an `@id` pointing to a section of a webpage (fragment identifier) but whose `@type` does NOT include `WebPageElement`. The MAY check emits an `sh:Info` suggestion.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "data.csv" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Tabular data", + "description": "A CSV file.", + "encodingFormat": [ + "text/csv", + { + "@id": "https://example.org/encoding-formats.html#csv" + } + ], + "contentSize": "1024" + }, + { + "@id": "https://example.org/encoding-formats.html#csv", + "@type": "WebPage", + "name": "CSV encoding format section", + "description": "Section of a webpage that describes the CSV encoding format (but not typed as WebPageElement)." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/encoding_format/no_fragment/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/encoding_format/no_fragment/ro-crate-metadata.json new file mode 100644 index 000000000..829ba2969 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/encoding_format/no_fragment/ro-crate-metadata.json @@ -0,0 +1,55 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Encoding format entity β€” no fragment (MAY does not apply)", + "description": "RO-Crate whose encoding format entity has an `@id` that is NOT a fragment identifier (does not point to a section of a webpage). The MAY shape should not apply and no `sh:Info` is emitted.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "data.csv" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Tabular data", + "description": "A CSV file.", + "encodingFormat": [ + "text/csv", + { + "@id": "https://example.org/csv-format.html" + } + ], + "contentSize": "1024" + }, + { + "@id": "https://example.org/csv-format.html", + "@type": "WebPage", + "name": "CSV encoding format page", + "description": "A full webpage describing the CSV encoding format (no fragment in @id)." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/encoding_format/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/encoding_format/valid/ro-crate-metadata.json new file mode 100644 index 000000000..7f21847a6 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/encoding_format/valid/ro-crate-metadata.json @@ -0,0 +1,55 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Encoding format entity β€” valid (WebPageElement)", + "description": "RO-Crate whose File references an encoding format entity whose `@id` points to a section of a webpage and whose `@type` includes `WebPageElement` (MAY condition satisfied).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "data.csv" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Tabular data", + "description": "A CSV file.", + "encodingFormat": [ + "text/csv", + { + "@id": "https://example.org/encoding-formats.html#csv" + } + ], + "contentSize": "1024" + }, + { + "@id": "https://example.org/encoding-formats.html#csv", + "@type": ["WebPage", "WebPageElement"], + "name": "CSV encoding format section", + "description": "Section of a webpage that describes the CSV encoding format." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_description/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_description/ro-crate-metadata.json new file mode 100644 index 000000000..5a5d0edcd --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_description/ro-crate-metadata.json @@ -0,0 +1,24 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "License entity β€” missing description", + "description": "RO-Crate whose license entity is missing the `description` property.", + "datePublished": "2024-01-01", + "license": { "@id": "https://spdx.org/licenses/MIT.html" } + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "CreativeWork", + "name": "MIT License" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_name/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_name/ro-crate-metadata.json new file mode 100644 index 000000000..a5984a717 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_name/ro-crate-metadata.json @@ -0,0 +1,24 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "License entity β€” missing name", + "description": "RO-Crate whose license entity is missing the `name` property.", + "datePublished": "2024-01-01", + "license": { "@id": "https://spdx.org/licenses/MIT.html" } + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "CreativeWork", + "description": "A short and simple permissive license with conditions only requiring preservation of copyright and license notices." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_type/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_type/ro-crate-metadata.json new file mode 100644 index 000000000..29ef7b495 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_type/ro-crate-metadata.json @@ -0,0 +1,25 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "License entity β€” missing CreativeWork type", + "description": "RO-Crate whose license entity has an absolute URL @id but is missing CreativeWork in @type.", + "datePublished": "2024-01-01", + "license": { "@id": "https://spdx.org/licenses/MIT.html" } + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "Thing", + "name": "MIT License", + "description": "A short and simple permissive license with conditions only requiring preservation of copyright and license notices." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_url/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_url/ro-crate-metadata.json new file mode 100644 index 000000000..f06f605c5 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/invalid_no_url/ro-crate-metadata.json @@ -0,0 +1,25 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "License entity β€” non-absolute URL @id", + "description": "RO-Crate whose license entity is identified by a fragment URI, not an absolute HTTP(S) URL.", + "datePublished": "2024-01-01", + "license": { "@id": "#local-license" } + }, + { + "@id": "#local-license", + "@type": "CreativeWork", + "name": "Local License", + "description": "A locally-defined license without an absolute URL identifier." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/valid/ro-crate-metadata.json new file mode 100644 index 000000000..b5ff18319 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/license_entity/valid/ro-crate-metadata.json @@ -0,0 +1,53 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "License entity β€” valid", + "description": "RO-Crate with a well-formed license entity: absolute HTTPS URL @id, CreativeWork type, name and description.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://spdx.org/licenses/MIT.html" + }, + "funder": { + "@id": "https://ror.org/03yrm5c26" + }, + "publisher": { + "@id": "https://ror.org/03yrm5c26" + } + }, + { + "@id": "https://ror.org/03yrm5c26", + "@type": "Organization", + "name": "Wellcome Trust", + "description": "The Wellcome Trust is a global charitable foundation dedicated to improving health by supporting scientific research and innovation.", + "url": "https://wellcome.org/", + "contactPoint": { + "@id": "mailto:info@wellcome.org" + } + }, + { + "@id": "mailto:info@wellcome.org", + "@type": "ContactPoint", + "contactType": "general inquiries", + "name": "Wellcome Trust Contact" + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "CreativeWork", + "name": "MIT License", + "description": "A short and simple permissive license with conditions only requiring preservation of copyright and license notices." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_contactpoint_no_contactpoint_entity/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_contactpoint_no_contactpoint_entity/ro-crate-metadata.json new file mode 100644 index 000000000..0a17e92e3 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_contactpoint_no_contactpoint_entity/ro-crate-metadata.json @@ -0,0 +1,51 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Organization entity β€” contactPoint not ContactPoint type", + "description": "RO-Crate with an Organization whose contactPoint does not reference a ContactPoint entity.", + "datePublished": "2024-01-01", + "license": { "@id": "https://spdx.org/licenses/MIT.html" }, + "publisher": { "@id": "https://ror.org/03f0f6041" }, + "author": { "@id": "https://orcid.org/0000-0001-6121-5409" } + }, + { + "@id": "https://ror.org/03f0f6041", + "@type": "Organization", + "name": "University of Technology Sydney", + "url": "https://ror.org/03f0f6041", + "contactPoint": { "@id": "#not-a-contactpoint" } + }, + { + "@id": "#not-a-contactpoint", + "@type": "Thing", + "name": "Not a ContactPoint" + }, + { + "@id": "https://orcid.org/0000-0001-6121-5409", + "@type": "Person", + "name": "Tim Luckett", + "contactPoint": { "@id": "mailto:tim.luckett@uts.edu.au" } + }, + { + "@id": "mailto:tim.luckett@uts.edu.au", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "tim.luckett@uts.edu.au" + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "CreativeWork", + "name": "MIT License", + "description": "A permissive license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_no_author_publisher_contactpoint/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_no_author_publisher_contactpoint/ro-crate-metadata.json new file mode 100644 index 000000000..446b7a5d8 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_no_author_publisher_contactpoint/ro-crate-metadata.json @@ -0,0 +1,38 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Organization entity β€” no author/publisher contactPoint", + "description": "RO-Crate where neither author nor publisher has a contactPoint property.", + "datePublished": "2024-01-01", + "license": { "@id": "https://spdx.org/licenses/MIT.html" }, + "publisher": { "@id": "https://ror.org/03f0f6041" }, + "author": { "@id": "https://orcid.org/0000-0001-6121-5409" } + }, + { + "@id": "https://ror.org/03f0f6041", + "@type": "Organization", + "name": "University of Technology Sydney", + "url": "https://ror.org/03f0f6041" + }, + { + "@id": "https://orcid.org/0000-0001-6121-5409", + "@type": "Person", + "name": "Tim Luckett" + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "CreativeWork", + "name": "MIT License", + "description": "A permissive license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_no_ror_id/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_no_ror_id/ro-crate-metadata.json new file mode 100644 index 000000000..d4ea0fe27 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_no_ror_id/ro-crate-metadata.json @@ -0,0 +1,52 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Organization entity β€” non-ROR @id", + "description": "RO-Crate with an Organization entity whose @id is not a ROR identifier.", + "datePublished": "2024-01-01", + "license": { "@id": "https://spdx.org/licenses/MIT.html" }, + "publisher": { "@id": "https://example.com/org" }, + "author": { "@id": "https://orcid.org/0000-0001-6121-5409" } + }, + { + "@id": "https://example.com/org", + "@type": "Organization", + "name": "Example Organization", + "url": "https://example.com/org", + "contactPoint": { "@id": "mailto:contact@example.com" } + }, + { + "@id": "mailto:contact@example.com", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "contact@example.com" + }, + { + "@id": "https://orcid.org/0000-0001-6121-5409", + "@type": "Person", + "name": "Tim Luckett", + "contactPoint": { "@id": "mailto:tim.luckett@uts.edu.au" } + }, + { + "@id": "mailto:tim.luckett@uts.edu.au", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "tim.luckett@uts.edu.au" + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "CreativeWork", + "name": "MIT License", + "description": "A permissive license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_org_no_contactpoint/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_org_no_contactpoint/ro-crate-metadata.json new file mode 100644 index 000000000..34536d50d --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/invalid_org_no_contactpoint/ro-crate-metadata.json @@ -0,0 +1,45 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Organization entity β€” no contactPoint", + "description": "RO-Crate with an Organization entity that has a ROR @id but no contactPoint property.", + "datePublished": "2024-01-01", + "license": { "@id": "https://spdx.org/licenses/MIT.html" }, + "publisher": { "@id": "https://ror.org/03f0f6041" }, + "author": { "@id": "https://orcid.org/0000-0001-6121-5409" } + }, + { + "@id": "https://ror.org/03f0f6041", + "@type": "Organization", + "name": "University of Technology Sydney", + "url": "https://ror.org/03f0f6041" + }, + { + "@id": "https://orcid.org/0000-0001-6121-5409", + "@type": "Person", + "name": "Tim Luckett", + "contactPoint": { "@id": "mailto:tim.luckett@uts.edu.au" } + }, + { + "@id": "mailto:tim.luckett@uts.edu.au", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "tim.luckett@uts.edu.au" + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "CreativeWork", + "name": "MIT License", + "description": "A permissive license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/valid/ro-crate-metadata.json new file mode 100644 index 000000000..f9ce66138 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/organization_entity/valid/ro-crate-metadata.json @@ -0,0 +1,74 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Organization entity β€” valid", + "description": "RO-Crate with a well-formed Organization entity: ROR @id, contactPoint referencing ContactPoint, and author with contactPoint.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://spdx.org/licenses/MIT.html" + }, + "publisher": { + "@id": "https://ror.org/03f0f6041" + }, + "author": { + "@id": "https://orcid.org/0000-0001-6121-5409" + }, + "funder": { + "@id": "https://ror.org/03f0f6041" + } + }, + { + "@id": "https://ror.org/03f0f6041", + "@type": "Organization", + "name": "University of Technology Sydney", + "url": "https://ror.org/03f0f6041", + "contactPoint": { + "@id": "mailto:contact@uts.edu.au" + } + }, + { + "@id": "mailto:contact@uts.edu.au", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "contact@uts.edu.au", + "name": "UTS contact point" + }, + { + "@id": "https://orcid.org/0000-0001-6121-5409", + "@type": "Person", + "name": "Tim Luckett", + "contactPoint": { + "@id": "mailto:tim.luckett@uts.edu.au" + }, + "affiliation": { + "@id": "https://ror.org/03f0f6041" + } + }, + { + "@id": "mailto:tim.luckett@uts.edu.au", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "tim.luckett@uts.edu.au", + "name": "Tim Luckett's contact point" + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "CreativeWork", + "name": "MIT License", + "description": "A permissive license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/person_entity/invalid_affiliation_not_org/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/person_entity/invalid_affiliation_not_org/ro-crate-metadata.json new file mode 100644 index 000000000..231cf10f2 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/person_entity/invalid_affiliation_not_org/ro-crate-metadata.json @@ -0,0 +1,57 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Person entity β€” invalid (affiliation not Organization)", + "description": "RO-Crate with a Person entity whose affiliation references a Thing instead of an Organization.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://spdx.org/licenses/MIT.html" + }, + "author": { + "@id": "https://orcid.org/0000-0001-6121-5409" + } + }, + { + "@id": "https://orcid.org/0000-0001-6121-5409", + "@type": "Person", + "name": "Tim Luckett", + "contactPoint": { + "@id": "mailto:tim.luckett@uts.edu.au" + }, + "affiliation": { + "@id": "https://example.org/groups/research-lab" + } + }, + { + "@id": "mailto:tim.luckett@uts.edu.au", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "tim.luckett@uts.edu.au", + "name": "Tim Luckett's contact point" + }, + { + "@id": "https://example.org/groups/research-lab", + "@type": "Thing", + "name": "Research Lab (not an Organization)" + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "CreativeWork", + "name": "MIT License", + "description": "A permissive license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/person_entity/invalid_no_orcid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/person_entity/invalid_no_orcid/ro-crate-metadata.json new file mode 100644 index 000000000..6a3895c46 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/person_entity/invalid_no_orcid/ro-crate-metadata.json @@ -0,0 +1,67 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Person entity β€” invalid (no ORCID @id)", + "description": "RO-Crate with a Person entity whose @id is not an ORCID identifier.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://spdx.org/licenses/MIT.html" + }, + "author": { + "@id": "https://example.org/persons/john-doe" + } + }, + { + "@id": "https://example.org/persons/john-doe", + "@type": "Person", + "name": "John Doe", + "contactPoint": { + "@id": "mailto:john.doe@example.org" + }, + "affiliation": { + "@id": "https://ror.org/03f0f6041" + } + }, + { + "@id": "mailto:john.doe@example.org", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "john.doe@example.org", + "name": "John Doe's contact point" + }, + { + "@id": "https://ror.org/03f0f6041", + "@type": "Organization", + "name": "University of Technology Sydney", + "contactPoint": { + "@id": "mailto:contact@uts.edu.au" + } + }, + { + "@id": "mailto:contact@uts.edu.au", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "contact@uts.edu.au", + "name": "UTS contact point" + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "CreativeWork", + "name": "MIT License", + "description": "A permissive license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/person_entity/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/person_entity/valid/ro-crate-metadata.json new file mode 100644 index 000000000..0181aba78 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/person_entity/valid/ro-crate-metadata.json @@ -0,0 +1,68 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Person entity β€” valid", + "description": "RO-Crate with a well-formed Person entity: ORCID @id, contactPoint referencing ContactPoint, and affiliation referencing Organization.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://spdx.org/licenses/MIT.html" + }, + "author": { + "@id": "https://orcid.org/0000-0001-6121-5409" + } + }, + { + "@id": "https://orcid.org/0000-0001-6121-5409", + "@type": "Person", + "name": "Tim Luckett", + "contactPoint": { + "@id": "mailto:tim.luckett@uts.edu.au" + }, + "affiliation": { + "@id": "https://ror.org/03f0f6041" + } + }, + { + "@id": "mailto:tim.luckett@uts.edu.au", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "tim.luckett@uts.edu.au", + "name": "Tim Luckett's contact point" + }, + { + "@id": "https://ror.org/03f0f6041", + "@type": "Organization", + "name": "University of Technology Sydney", + "url": "https://www.uts.edu.au/", + "contactPoint": { + "@id": "mailto:contact@uts.edu.au" + } + }, + { + "@id": "mailto:contact@uts.edu.au", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "contact@uts.edu.au", + "name": "UTS contact point" + }, + { + "@id": "https://spdx.org/licenses/MIT.html", + "@type": "CreativeWork", + "name": "MIT License", + "description": "A permissive license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/invalid_no_name/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/invalid_no_name/ro-crate-metadata.json new file mode 100644 index 000000000..86e06da74 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/invalid_no_name/ro-crate-metadata.json @@ -0,0 +1,49 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "SoftwareApplication entity β€” invalid (no name)", + "description": "RO-Crate referencing a SoftwareApplication contextual entity that does NOT declare a `name` property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "mentions": { + "@id": "#create-action" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "#create-action", + "@type": "CreateAction", + "name": "Run analysis tool", + "instrument": { + "@id": "#analysis-tool" + } + }, + { + "@id": "#analysis-tool", + "@type": "SoftwareApplication", + "version": "1.4.2", + "url": "https://example.org/analysis-tool", + "description": "Software used to perform the data analysis (missing `name`)." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/invalid_no_url/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/invalid_no_url/ro-crate-metadata.json new file mode 100644 index 000000000..9fb04a670 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/invalid_no_url/ro-crate-metadata.json @@ -0,0 +1,49 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "SoftwareApplication entity β€” invalid (no url)", + "description": "RO-Crate referencing a SoftwareApplication contextual entity that does NOT declare a `url` property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "mentions": { + "@id": "#create-action" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "#create-action", + "@type": "CreateAction", + "name": "Run analysis tool", + "instrument": { + "@id": "#analysis-tool" + } + }, + { + "@id": "#analysis-tool", + "@type": "SoftwareApplication", + "name": "Analysis Tool", + "version": "1.4.2", + "description": "Software used to perform the data analysis (missing `url`)." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/invalid_no_version/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/invalid_no_version/ro-crate-metadata.json new file mode 100644 index 000000000..ec8133140 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/invalid_no_version/ro-crate-metadata.json @@ -0,0 +1,49 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "SoftwareApplication entity β€” invalid (no version)", + "description": "RO-Crate referencing a SoftwareApplication contextual entity that does NOT declare a version property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "mentions": { + "@id": "#create-action" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "#create-action", + "@type": "CreateAction", + "name": "Run analysis tool", + "instrument": { + "@id": "#analysis-tool" + } + }, + { + "@id": "#analysis-tool", + "@type": "SoftwareApplication", + "name": "Analysis Tool", + "url": "https://example.org/analysis-tool", + "description": "Software used to perform the data analysis (missing `version`)." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid/ro-crate-metadata.json new file mode 100644 index 000000000..dd9796a57 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid/ro-crate-metadata.json @@ -0,0 +1,71 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "SoftwareApplication entity β€” valid", + "description": "RO-Crate referencing a SoftwareApplication contextual entity that declares a version (used as instrument of a CreateAction).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "mentions": { + "@id": "#create-action" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#org-contact" + } + }, + { + "@id": "#org-contact", + "@type": "ContactPoint", + "name": "Example Research Institute Contact", + "email": "contact@example.org" + }, + { + "@id": "#create-action", + "@type": "CreateAction", + "name": "Run analysis tool", + "instrument": { + "@id": "#analysis-tool" + } + }, + { + "@id": "#analysis-tool", + "@type": "SoftwareApplication", + "name": "Analysis Tool", + "version": "1.4.2", + "url": "https://example.org/analysis-tool", + "description": "Software used to perform the data analysis." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid_computer_language/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid_computer_language/ro-crate-metadata.json new file mode 100644 index 000000000..d1cd32346 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid_computer_language/ro-crate-metadata.json @@ -0,0 +1,76 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "ComputerLanguage entity β€” valid", + "description": "RO-Crate referencing a ComputerLanguage contextual entity (used as `programmingLanguage` of a Script) that declares name, url, version, and an optional alternateName.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": [ + { + "@id": "script.sh" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#org-contact" + } + }, + { + "@id": "#org-contact", + "@type": "ContactPoint", + "name": "Example Research Institute Contact", + "email": "contact@example.org" + }, + { + "@id": "script.sh", + "@type": ["File", "SoftwareSourceCode"], + "name": "Analysis script", + "description": "A Bash analysis script.", + "encodingFormat": "application/x-sh", + "contentSize": "256", + "programmingLanguage": { + "@id": "#bash" + } + }, + { + "@id": "#bash", + "@type": "ComputerLanguage", + "name": "Bash", + "alternateName": "GNU Bourne-Again Shell", + "url": "https://www.gnu.org/software/bash/", + "version": "5.2" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid_computer_language/script.sh b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid_computer_language/script.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid_with_alternatename/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid_with_alternatename/ro-crate-metadata.json new file mode 100644 index 000000000..f2a452552 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/10_metadata_contextualEntities/software_application/valid_with_alternatename/ro-crate-metadata.json @@ -0,0 +1,60 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "ComputerLanguage with alternateName β€” fully valid", + "description": "RO-Crate referencing a ComputerLanguage contextual entity that declares all MUST properties (`name`, `url`, `version`) AND the MAY `alternateName`. The crate is minimal β€” it contains no File/Directory data entities β€” so no OPTIONAL-level INFO checks should fire.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "mentions": { + "@id": "#bash" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#org-contact" + } + }, + { + "@id": "#org-contact", + "@type": "ContactPoint", + "name": "Example Research Institute Contact", + "email": "contact@example.org" + }, + { + "@id": "#bash", + "@type": "ComputerLanguage", + "name": "Bash", + "alternateName": "GNU Bourne-Again Shell", + "url": "https://www.gnu.org/software/bash/", + "version": "5.2" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/invalid/diagram.png b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/invalid/diagram.png new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..813557ec3 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/invalid/ro-crate-metadata.json @@ -0,0 +1,51 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Image about β€” invalid (missing about)", + "description": "RO-Crate with a Workflow whose image ImageObject is missing the recommended about property.", + "datePublished": "2024-01-01", + "license": { "@id": "https://creativecommons.org/licenses/by/4.0/" }, + "hasPart": [ + { "@id": "workflow.ga" }, + { "@id": "diagram.png" } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "workflow.ga", + "@type": ["File", "SoftwareSourceCode", "ComputationalWorkflow"], + "name": "My Workflow", + "description": "A Galaxy workflow with a diagram image.", + "encodingFormat": "application/json", + "programmingLanguage": { "@id": "#galaxy" }, + "conformsTo": { "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" }, + "image": { "@id": "diagram.png" } + }, + { + "@id": "#galaxy", + "@type": "ComputerLanguage", + "name": "Galaxy", + "url": { "@id": "https://galaxyproject.org/" } + }, + { + "@id": "diagram.png", + "@type": ["File", "ImageObject"], + "name": "Workflow diagram", + "description": "A diagram missing the recommended about property.", + "encodingFormat": "image/png" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/invalid/workflow.ga b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/invalid/workflow.ga new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/valid/diagram.png b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/valid/diagram.png new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/valid/ro-crate-metadata.json new file mode 100644 index 000000000..aa7b84e36 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/valid/ro-crate-metadata.json @@ -0,0 +1,124 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Image about \u2014 valid", + "description": "RO-Crate with a Workflow whose image ImageObject has the recommended about property referencing the workflow.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "hasPart": [ + { + "@id": "workflow.ga" + }, + { + "@id": "diagram.png" + } + ], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0", + "description": "Creative Commons Attribution 4.0 International license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example University", + "url": "https://example.edu", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Example University Contact", + "email": "contact@example.edu" + }, + { + "@id": "workflow.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "name": "My Workflow", + "description": "A Galaxy workflow with a diagram image.", + "encodingFormat": "application/json", + "contentSize": "2048", + "programmingLanguage": { + "@id": "#galaxy" + }, + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "image": { + "@id": "diagram.png" + } + }, + { + "@id": "#galaxy", + "@type": "ComputerLanguage", + "name": "Galaxy", + "url": "https://galaxyproject.org/", + "version": "1.0" + }, + { + "@id": "diagram.png", + "@type": [ + "File", + "ImageObject" + ], + "name": "Workflow diagram", + "description": "A diagram with about referencing the workflow.", + "encodingFormat": "image/png", + "contentSize": "10240", + "about": { + "@id": "workflow.ga" + } + }, + { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Bioschemas ComputationalWorkflow Profile 1.0-RELEASE", + "description": "Bioschemas profile for computational workflows." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/valid/workflow.ga b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_about/valid/workflow.ga new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/invalid/diagram.png b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/invalid/diagram.png new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..a501f242b --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/invalid/ro-crate-metadata.json @@ -0,0 +1,51 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Image encodingFormat β€” invalid (missing encodingFormat)", + "description": "RO-Crate with a Workflow whose image ImageObject is missing the recommended encodingFormat property.", + "datePublished": "2024-01-01", + "license": { "@id": "https://creativecommons.org/licenses/by/4.0/" }, + "hasPart": [ + { "@id": "workflow.ga" }, + { "@id": "diagram.png" } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "workflow.ga", + "@type": ["File", "SoftwareSourceCode", "ComputationalWorkflow"], + "name": "My Workflow", + "description": "A Galaxy workflow with a diagram image.", + "encodingFormat": "application/json", + "programmingLanguage": { "@id": "#galaxy" }, + "conformsTo": { "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" }, + "image": { "@id": "diagram.png" } + }, + { + "@id": "#galaxy", + "@type": "ComputerLanguage", + "name": "Galaxy", + "url": { "@id": "https://galaxyproject.org/" } + }, + { + "@id": "diagram.png", + "@type": ["File", "ImageObject"], + "name": "Workflow diagram", + "description": "A diagram missing the required encodingFormat.", + "about": { "@id": "workflow.ga" } + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/invalid/workflow.ga b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/invalid/workflow.ga new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/valid/diagram.png b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/valid/diagram.png new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/valid/ro-crate-metadata.json new file mode 100644 index 000000000..9c332570e --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/valid/ro-crate-metadata.json @@ -0,0 +1,124 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Image encodingFormat \u2014 valid", + "description": "RO-Crate with a Workflow whose image ImageObject has the recommended encodingFormat property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "hasPart": [ + { + "@id": "workflow.ga" + }, + { + "@id": "diagram.png" + } + ], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0", + "description": "Creative Commons Attribution 4.0 International license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example University", + "url": "https://example.edu", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Example University Contact", + "email": "contact@example.edu" + }, + { + "@id": "workflow.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "name": "My Workflow", + "description": "A Galaxy workflow with a diagram image.", + "encodingFormat": "application/json", + "contentSize": "2048", + "programmingLanguage": { + "@id": "#galaxy" + }, + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "image": { + "@id": "diagram.png" + } + }, + { + "@id": "#galaxy", + "@type": "ComputerLanguage", + "name": "Galaxy", + "url": "https://galaxyproject.org/", + "version": "1.0" + }, + { + "@id": "diagram.png", + "@type": [ + "File", + "ImageObject" + ], + "name": "Workflow diagram", + "description": "A diagram illustrating the workflow steps.", + "encodingFormat": "image/png", + "contentSize": "10240", + "about": { + "@id": "workflow.ga" + } + }, + { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Bioschemas ComputationalWorkflow Profile 1.0-RELEASE", + "description": "Bioschemas profile for computational workflows." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/valid/workflow.ga b/tests/data/crates/rocrate-1.3/11_workflows_scripts/image_encoding_format/valid/workflow.ga new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..daec94202 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/invalid/ro-crate-metadata.json @@ -0,0 +1,32 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Programming language β€” invalid (missing programmingLanguage)", + "description": "RO-Crate with a Script that is missing the recommended programmingLanguage property.", + "datePublished": "2024-01-01", + "license": { "@id": "https://creativecommons.org/licenses/by/4.0/" }, + "hasPart": [{ "@id": "script.sh" }] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "script.sh", + "@type": ["File", "SoftwareSourceCode"], + "name": "My Script", + "description": "A script without a programmingLanguage property.", + "encodingFormat": "application/x-sh" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/invalid/script.sh b/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/invalid/script.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json new file mode 100644 index 000000000..880550b45 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/valid/ro-crate-metadata.json @@ -0,0 +1,86 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Programming language \u2014 valid", + "description": "RO-Crate with a Script that declares programmingLanguage referencing a ComputerLanguage entity.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": { + "@id": "script.sh" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0", + "description": "Creative Commons Attribution 4.0 International license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example University", + "url": "https://example.edu", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Example University Contact", + "email": "contact@example.edu" + }, + { + "@id": "script.sh", + "@type": [ + "File", + "SoftwareSourceCode" + ], + "name": "My Script", + "description": "A script with proper programmingLanguage declaration.", + "encodingFormat": "application/x-sh", + "contentSize": "1024", + "programmingLanguage": { + "@id": "#bash" + } + }, + { + "@id": "#bash", + "@type": "ComputerLanguage", + "name": "Bash", + "url": "https://www.gnu.org/software/bash/", + "version": "1.0" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/valid/script.sh b/tests/data/crates/rocrate-1.3/11_workflows_scripts/programming_language/valid/script.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..9dcbc2843 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/invalid/ro-crate-metadata.json @@ -0,0 +1,31 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Script name β€” invalid (missing name)", + "description": "RO-Crate with a Script entity that is missing the required name property.", + "datePublished": "2024-01-01", + "license": { "@id": "https://creativecommons.org/licenses/by/4.0/" }, + "hasPart": [{ "@id": "script.sh" }] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "script.sh", + "@type": ["File", "SoftwareSourceCode"], + "description": "A script missing its required name property.", + "encodingFormat": "application/x-sh" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/invalid/script.sh b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/invalid/script.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/valid/ro-crate-metadata.json new file mode 100644 index 000000000..48fe94ed7 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/valid/ro-crate-metadata.json @@ -0,0 +1,57 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Script name \u2014 valid", + "description": "RO-Crate containing a Script entity with a required name property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "script.sh" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "script.sh", + "@type": [ + "File", + "SoftwareSourceCode" + ], + "name": "My Script", + "description": "A sample shell script with a proper name.", + "encodingFormat": "application/x-sh", + "programmingLanguage": { + "@id": "#bash" + } + }, + { + "@id": "#bash", + "@type": "ComputerLanguage", + "name": "Bash", + "url": { + "@id": "https://www.gnu.org/software/bash/" + }, + "version": "1.0" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/valid/script.sh b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_name/valid/script.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..e201ae943 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/invalid/ro-crate-metadata.json @@ -0,0 +1,30 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Script type β€” invalid (missing File type)", + "description": "RO-Crate with a Script entity missing the required File type.", + "datePublished": "2024-01-01", + "license": { "@id": "https://creativecommons.org/licenses/by/4.0/" } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "https://example.org/script.sh", + "@type": "SoftwareSourceCode", + "name": "My Script", + "description": "A script with SoftwareSourceCode type only β€” missing the required File type." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/valid/ro-crate-metadata.json new file mode 100644 index 000000000..f60a9f3ca --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/valid/ro-crate-metadata.json @@ -0,0 +1,57 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Script type \u2014 valid", + "description": "RO-Crate containing a Script entity with correct @type [File, SoftwareSourceCode].", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "script.sh" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "script.sh", + "@type": [ + "File", + "SoftwareSourceCode" + ], + "name": "My Script", + "description": "A sample shell script.", + "encodingFormat": "application/x-sh", + "programmingLanguage": { + "@id": "#bash" + } + }, + { + "@id": "#bash", + "@type": "ComputerLanguage", + "name": "Bash", + "url": { + "@id": "https://www.gnu.org/software/bash/" + }, + "version": "1.0" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/valid/script.sh b/tests/data/crates/rocrate-1.3/11_workflows_scripts/script_type/valid/script.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..7eec1660a --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/invalid/ro-crate-metadata.json @@ -0,0 +1,39 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Workflow conformsTo β€” invalid (missing Bioschemas conformsTo)", + "description": "RO-Crate with a Workflow missing the recommended conformsTo Bioschemas profile.", + "datePublished": "2024-01-01", + "license": { "@id": "https://creativecommons.org/licenses/by/4.0/" }, + "hasPart": [{ "@id": "workflow.ga" }] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "workflow.ga", + "@type": ["File", "SoftwareSourceCode", "ComputationalWorkflow"], + "name": "My Workflow", + "description": "A workflow without the recommended Bioschemas conformsTo declaration.", + "encodingFormat": "application/json", + "programmingLanguage": { "@id": "#galaxy" } + }, + { + "@id": "#galaxy", + "@type": "ComputerLanguage", + "name": "Galaxy", + "url": { "@id": "https://galaxyproject.org/" } + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/invalid/script.sh b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/invalid/script.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/invalid/workflow.ga b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/invalid/workflow.ga new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json new file mode 100644 index 000000000..2709cc83a --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/ro-crate-metadata.json @@ -0,0 +1,102 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Workflow conformsTo \u2014 valid", + "description": "RO-Crate with a Workflow that declares conformsTo referencing a versioned Bioschemas profile URI.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "hasPart": { + "@id": "workflow.ga" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0", + "description": "Creative Commons Attribution 4.0 International license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example University", + "url": "https://example.edu", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Example University Contact", + "email": "contact@example.edu" + }, + { + "@id": "workflow.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "name": "My Workflow", + "description": "A workflow conforming to the Bioschemas ComputationalWorkflow profile.", + "encodingFormat": "application/json", + "contentSize": "2048", + "programmingLanguage": { + "@id": "#galaxy" + }, + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + } + }, + { + "@id": "#galaxy", + "@type": "ComputerLanguage", + "name": "Galaxy", + "url": "https://galaxyproject.org/", + "version": "1.0" + }, + { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Bioschemas ComputationalWorkflow Profile 1.0-RELEASE", + "description": "Bioschemas profile for computational workflows." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/script.sh b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/script.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/workflow.ga b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_conformsTo/valid/workflow.ga new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..f513ae717 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/invalid/ro-crate-metadata.json @@ -0,0 +1,31 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Workflow name β€” invalid (missing name)", + "description": "RO-Crate with a Workflow entity missing the required name property.", + "datePublished": "2024-01-01", + "license": { "@id": "https://creativecommons.org/licenses/by/4.0/" }, + "hasPart": [{ "@id": "workflow.ga" }] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "workflow.ga", + "@type": ["File", "SoftwareSourceCode", "ComputationalWorkflow"], + "description": "A workflow missing its required name property.", + "encodingFormat": "application/json" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/invalid/script.sh b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/invalid/script.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/invalid/workflow.ga b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/invalid/workflow.ga new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/valid/ro-crate-metadata.json new file mode 100644 index 000000000..94ad2d688 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/valid/ro-crate-metadata.json @@ -0,0 +1,61 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Workflow name \u2014 valid", + "description": "RO-Crate with a Workflow entity that has the required name property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "workflow.ga" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "workflow.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "name": "My Workflow", + "description": "A Galaxy workflow with all required types and name.", + "encodingFormat": "application/json", + "programmingLanguage": { + "@id": "#galaxy" + }, + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + } + }, + { + "@id": "#galaxy", + "@type": "ComputerLanguage", + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "1.0" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/valid/script.sh b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/valid/script.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/valid/workflow.ga b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_name/valid/workflow.ga new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/invalid_missing_file/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/invalid_missing_file/ro-crate-metadata.json new file mode 100644 index 000000000..0aea36a81 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/invalid_missing_file/ro-crate-metadata.json @@ -0,0 +1,30 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Workflow type β€” invalid (missing File type)", + "description": "RO-Crate with a Workflow entity missing the required File type.", + "datePublished": "2024-01-01", + "license": { "@id": "https://creativecommons.org/licenses/by/4.0/" } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "https://example.org/workflow.ga", + "@type": ["SoftwareSourceCode", "ComputationalWorkflow"], + "name": "My Workflow", + "description": "A workflow missing the required File type." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/invalid_missing_ssc/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/invalid_missing_ssc/ro-crate-metadata.json new file mode 100644 index 000000000..645a9f1a8 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/invalid_missing_ssc/ro-crate-metadata.json @@ -0,0 +1,30 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { "@id": "./" }, + "conformsTo": { "@id": "https://w3id.org/ro/crate/1.3" } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Workflow type β€” invalid (missing SoftwareSourceCode type)", + "description": "RO-Crate with a Workflow entity missing the required SoftwareSourceCode type.", + "datePublished": "2024-01-01", + "license": { "@id": "https://creativecommons.org/licenses/by/4.0/" } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "https://example.org/workflow.ga", + "@type": ["File", "ComputationalWorkflow"], + "name": "My Workflow", + "description": "A workflow missing the required SoftwareSourceCode type." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/valid/ro-crate-metadata.json new file mode 100644 index 000000000..a11a1a3a6 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/valid/ro-crate-metadata.json @@ -0,0 +1,61 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Workflow type \u2014 valid", + "description": "RO-Crate with a Workflow entity correctly typed as [File, SoftwareSourceCode, ComputationalWorkflow].", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "workflow.ga" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "CC BY 4.0" + }, + { + "@id": "workflow.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "name": "My Workflow", + "description": "A sample Galaxy workflow.", + "encodingFormat": "application/json", + "programmingLanguage": { + "@id": "#galaxy" + }, + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + } + }, + { + "@id": "#galaxy", + "@type": "ComputerLanguage", + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "1.0" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/valid/script.sh b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/valid/script.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/valid/workflow.ga b/tests/data/crates/rocrate-1.3/11_workflows_scripts/workflow_type/valid/workflow.ga new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/invalid/data.csv b/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/invalid/data.csv new file mode 100644 index 000000000..9e1228cfb --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/invalid/data.csv @@ -0,0 +1,2 @@ +id,value +1,a diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..a9fd3afef --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/invalid/ro-crate-metadata.json @@ -0,0 +1,52 @@ +{ + "@context": "https://w3id.org/ro/crate/1.4-DRAFT/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.4-DRAFT" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Example: contextual entity linked from other entities", + "description": "This RO-Crate demonstrates the RO-Crate 1.3 requirement that any contextual entity in the @graph SHOULD be linked to from at least one of the other entities using its @id. The Person entity below is correctly referenced from the Root Data Entity via the author property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0000-0002-1825-0097" + }, + "hasPart": [ + { + "@id": "data.csv" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://orcid.org/0000-0002-1825-0097", + "@type": "Person", + "name": "Josiah Carberry" + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data", + "description": "A sample data file.", + "encodingFormat": "text/csv", + "contentSize": "42" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/valid/data.csv b/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/valid/data.csv new file mode 100644 index 000000000..9e1228cfb --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/valid/data.csv @@ -0,0 +1,2 @@ +id,value +1,a diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/valid/ro-crate-metadata.json new file mode 100644 index 000000000..a18c32691 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/context_reference/valid/ro-crate-metadata.json @@ -0,0 +1,52 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3/" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Example: contextual entity linked from other entities", + "description": "This RO-Crate demonstrates the RO-Crate 1.3 requirement that any contextual entity in the @graph SHOULD be linked to from at least one of the other entities using its @id. The Person entity below is correctly referenced from the Root Data Entity via the author property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0000-0002-1825-0097" + }, + "hasPart": [ + { + "@id": "data.csv" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://orcid.org/0000-0002-1825-0097", + "@type": "Person", + "name": "Josiah Carberry" + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data", + "description": "A sample data file.", + "encodingFormat": "text/csv", + "contentSize": "42" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/invalid/data.csv b/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/invalid/data.csv new file mode 100644 index 000000000..9e1228cfb --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/invalid/data.csv @@ -0,0 +1,2 @@ +id,value +1,a diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..2c5d41597 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/invalid/ro-crate-metadata.json @@ -0,0 +1,41 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Example: contextual entity linked from other entities", + "description": "This RO-Crate demonstrates the RO-Crate 1.3 requirement that any contextual entity in the @graph SHOULD be linked to from at least one of the other entities using its @id. The Person entity below is correctly referenced from the Root Data Entity via the author property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0000-0002-1825-0097" + }, + "hasPart": [ + { + "@id": "data.csv" + } + ] + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data", + "description": "A sample data file.", + "encodingFormat": "text/csv", + "contentSize": "42" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/valid/data.csv b/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/valid/data.csv new file mode 100644 index 000000000..9e1228cfb --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/valid/data.csv @@ -0,0 +1,2 @@ +id,value +1,a diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/valid/ro-crate-metadata.json new file mode 100644 index 000000000..8cd97e31b --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/described_contextual_entities/valid/ro-crate-metadata.json @@ -0,0 +1,94 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Example: contextual entity linked from other entities", + "description": "This RO-Crate demonstrates the RO-Crate 1.3 requirement that any contextual entity in the @graph SHOULD be linked to from at least one of the other entities using its @id. The Person entity below is correctly referenced from the Root Data Entity via the author property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0000-0002-1825-0097" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": { + "@id": "data.csv" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://orcid.org/0000-0002-1825-0097", + "@type": "Person", + "name": "Josiah Carberry", + "contactPoint": { + "@id": "#josiah-carberry-contact" + }, + "affiliation": { + "@id": "https://ror.org/05f9q8d28" + } + }, + { + "@id": "#josiah-carberry-contact", + "@type": "ContactPoint", + "name": "Josiah Carberry Contact", + "email": "mailto:josiah.carberry@example.com" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example University", + "description": "An example university that the Person entity is affiliated with.", + "url": "https://www.exampleuniversity.edu", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data", + "description": "A sample data file.", + "encodingFormat": "text/csv", + "contentSize": "42" + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example University", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/data set/.gitkeep b/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/data set/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/data set2/.gitkeep b/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/data set2/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/data set3/.gitkeep b/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/data set3/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/pics/2017-06-11%2012.56.14.jpg b/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/pics/2017-06-11%2012.56.14.jpg new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/pics/2018-06-11 12.56.14.jpg b/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/pics/2018-06-11 12.56.14.jpg new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/pics/2019-06-11 12.56.14.jpg b/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/pics/2019-06-11 12.56.14.jpg new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/pics/sepia_fence.jpg b/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/pics/sepia_fence.jpg new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/ro-crate-metadata.json new file mode 100644 index 000000000..083bb7a8d --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/format/compacted/ro-crate-metadata.json @@ -0,0 +1,216 @@ +{ + "@context": [ + "https://w3id.org/ro/crate/1.3/context", + "https://w3id.org/ro/terms/workflow-run/context" + ], + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "https://schema.org/name": "My Pictures", + "description": "A collection of my pictures", + "datePublished": "2024-05-17T01:04:52+01:00", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate/1.3" + } + ], + "hasPart": [ + { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/2018-06-11%2012.56.14.jpg" + }, + { + "@id": "pics/2019-06-11 12.56.14.jpg" + }, + { + "@id": "data%20set/" + }, + { + "@id": "data%20set2/" + }, + { + "@id": "data set3/" + }, + { + "@id": "pics/sepia_fence.jpg" + }, + { + "@id": "file:///tmp/test.txt" + } + ], + "isBasedOn": { + "@id": "https://doi.org/10.5281/zenodo.1009240" + }, + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "mentions": { + "@id": "#SepiaConversion_1" + } + }, + { + "@id": "https://w3id.org/ro/wfrun/process/0.5", + "@type": "CreativeWork", + "name": "Process Run Crate", + "version": "0.5" + }, + { + "@id": "https://example.com/otherprofile/0.1", + "@type": "CreativeWork", + "name": "Other Profile", + "version": "0.1" + }, + { + "@id": "https://www.imagemagick.org/", + "@type": "SoftwareApplication", + "url": "https://www.imagemagick.org/", + "name": "ImageMagick", + "softwareVersion": "6.9.7-4", + "softwareRequirements": { + "@id": "https://example.com/foobar/1.0.0/" + }, + "version": "1.0" + }, + { + "@id": "https://example.com/foobar/1.0.0/", + "@type": "SoftwareApplication", + "name": "foobar", + "softwareVersion": "1.0.0", + "version": "1.0", + "url": "https://example.org/" + }, + { + "@id": "#SepiaConversion_1", + "@type": "CreateAction", + "name": "Convert dog image to sepia", + "description": "convert -sepia-tone 80% pics/2017-06-11\\ 12.56.14.jpg pics/sepia_fence.jpg", + "startTime": "2024-05-17T01:04:50+01:00", + "endTime": "2024-05-17T01:04:52+01:00", + "instrument": { + "@id": "https://www.imagemagick.org/" + }, + "object": { + "@id": "pics/2017-06-11%2012.56.14.jpg" + }, + "result": { + "@id": "pics/sepia_fence.jpg" + }, + "agent": { + "@id": "https://orcid.org/0000-0001-9842-9718" + }, + "actionStatus": "http://schema.org/FailedActionStatus", + "error": "this is just to test the error property", + "environment": [ + { + "@id": "#height-limit-pv" + }, + { + "@id": "#width-limit-pv" + } + ], + "containerImage": "https://example.com/imagemagick.sif" + }, + { + "@id": "#width-limit-pv", + "@type": "PropertyValue", + "name": "MAGICK_WIDTH_LIMIT", + "value": "4096" + }, + { + "@id": "#height-limit-pv", + "@type": "PropertyValue", + "name": "MAGICK_HEIGHT_LIMIT", + "value": "3072" + }, + { + "@id": "file:///tmp/test.txt", + "@type": "File", + "description": "A test file", + "encodingFormat": "text/plain" + }, + { + "@id": "pics/2017-06-11%2012.56.14.jpg", + "@type": "File", + "description": "Original image", + "encodingFormat": "image/jpeg", + "name": "2017-06-11 12.56.14.jpg (input)", + "author": { + "@id": "https://orcid.org/0000-0002-3545-944X" + } + }, + { + "@id": "pics/2018-06-11%2012.56.14.jpg", + "@type": "File", + "description": "Original image", + "encodingFormat": "image/jpeg", + "name": "2018-06-11 12.56.14.jpg (input)" + }, + { + "@id": "pics/2019-06-11 12.56.14.jpg", + "@type": "File", + "description": "Original image", + "encodingFormat": "image/jpeg", + "name": "2018-06-11 12.56.14.jpg (input)" + }, + { + "@id": "data%20set/", + "@type": "Dataset", + "name": "Data set", + "description": "A dataset", + "datePublished": "2024-05-17T01:04:52+01:00", + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + } + }, + { + "@id": "data%20set2/", + "@type": "Dataset", + "name": "Data set 2", + "description": "A dataset", + "datePublished": "2024-05-17T01:04:52+01:00", + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + } + }, + { + "@id": "data set3/", + "@type": "Dataset", + "name": "Data set 3", + "description": "A dataset", + "datePublished": "2024-05-17T01:04:52+01:00", + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + } + }, + { + "@id": "pics/sepia_fence.jpg", + "@type": "File", + "description": "The converted picture, now sepia-colored", + "encodingFormat": "image/jpeg", + "name": "sepia_fence (output)" + }, + { + "@id": "https://orcid.org/0000-0001-9842-9718", + "@type": "Person", + "name": "Stian Soiland-Reyes" + }, + { + "@id": "https://orcid.org/0000-0002-3545-944X", + "@type": "Person", + "name": "Peter Sefton" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/format/flattened/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/format/flattened/ro-crate-metadata.json new file mode 100644 index 000000000..7a3831b16 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/format/flattened/ro-crate-metadata.json @@ -0,0 +1,31 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@type": "CreativeWork", + "@id": "ro-crate-metadata.json", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "This is a test dataset", + "description": "This is a test dataset", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "datePublished": "2024-11-05", + "hasPart": [ + { + "@type": "File", + "name": "File in a nested entity" + } + ] + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/format/jsonld/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/format/jsonld/ro-crate-metadata.json new file mode 100644 index 000000000..5ed1378a0 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/format/jsonld/ro-crate-metadata.json @@ -0,0 +1,19 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "name": "Counterexample: invalid JSON-LD syntax", + "description": "This RO-Crate is INCORRECT because it contains invalid JSON syntax. The trailing comma after the last property and the missing closing brace make it invalid JSON, and therefore invalid JSON-LD." + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Invalid JSON-LD Example", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + } + ] diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/format/utf8/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/format/utf8/ro-crate-metadata.json new file mode 100644 index 000000000..a9372f409 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/format/utf8/ro-crate-metadata.json @@ -0,0 +1,25 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Locale: cafι rιsumι naοve", + "description": "This RO-Crate is INCORRECT because it contains non-UTF-8 characters.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/" + } + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/named_entity_id_format/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/named_entity_id_format/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..b342d366e --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/named_entity_id_format/invalid/ro-crate-metadata.json @@ -0,0 +1,38 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Named entity without #-prefix β€” invalid", + "description": "RO-Crate where a local Person entity uses a bare relative @id instead of '#'-prefixed.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "author": {"@id": "alice"}, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "alice", + "@type": "Person", + "name": "Alice Researcher", + "affiliation": {"@id": "example-org"} + }, + { + "@id": "example-org", + "@type": "Organization", + "name": "Example University" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/named_entity_id_format/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/named_entity_id_format/valid/ro-crate-metadata.json new file mode 100644 index 000000000..012c0355f --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/named_entity_id_format/valid/ro-crate-metadata.json @@ -0,0 +1,93 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Named entity #-prefix β€” valid", + "description": "RO-Crate where all local Person and Organization entities use '#'-prefixed @id.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "author": { + "@id": "https://orcid.org/0000-0001-0000-0001" + }, + "hasPart": [], + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://orcid.org/0000-0001-0000-0001", + "@type": "Person", + "name": "Alice Researcher", + "affiliation": { + "@id": "https://ror.org/05f9q8d28" + }, + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example University", + "url": "https://example.edu", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example University", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/no_parent_traversal/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/no_parent_traversal/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..731a063e7 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/no_parent_traversal/invalid/ro-crate-metadata.json @@ -0,0 +1,40 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Parent traversal β€” invalid", + "description": "RO-Crate with a contextual entity whose @id uses ../ to climb out of the RO-Crate Root.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "publisher": {"@id": "#publisher-org"}, + "hasPart": [], + "citation": {"@id": "../external-publication.html"} + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "../external-publication.html", + "@type": "ScholarlyArticle", + "name": "A publication outside the crate root", + "description": "A contextual entity referenced via ../ β€” demonstrates parent directory traversal." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/no_parent_traversal/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/no_parent_traversal/valid/ro-crate-metadata.json new file mode 100644 index 000000000..351fa0179 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/no_parent_traversal/valid/ro-crate-metadata.json @@ -0,0 +1,79 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "No parent traversal β€” valid", + "description": "RO-Crate with no @id values that use ../ to climb out of the root.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": [], + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/invalid/data.csv b/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/invalid/data.csv new file mode 100644 index 000000000..9e1228cfb --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/invalid/data.csv @@ -0,0 +1,2 @@ +id,value +1,a diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..a990ab0b2 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/invalid/ro-crate-metadata.json @@ -0,0 +1,49 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: orphaned contextual entity", + "description": "This RO-Crate INCORRECTLY includes a contextual entity (the Person below) that is NOT linked from any other entity in the @graph. In RO-Crate 1.3, any contextual entity in the @graph SHOULD be linked to from at least one of the other entities using its @id. The Person entity is orphaned - nothing references https://orcid.org/0000-0002-1825-0097.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "data.csv" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://orcid.org/0000-0002-1825-0097", + "@type": "Person", + "name": "Josiah Carberry" + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data", + "description": "A sample data file.", + "encodingFormat": "text/csv", + "contentSize": "42" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/valid/data.csv b/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/valid/data.csv new file mode 100644 index 000000000..9e1228cfb --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/valid/data.csv @@ -0,0 +1,2 @@ +id,value +1,a diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/valid/ro-crate-metadata.json new file mode 100644 index 000000000..8cd97e31b --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/referenced_contextual_entities/valid/ro-crate-metadata.json @@ -0,0 +1,94 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Example: contextual entity linked from other entities", + "description": "This RO-Crate demonstrates the RO-Crate 1.3 requirement that any contextual entity in the @graph SHOULD be linked to from at least one of the other entities using its @id. The Person entity below is correctly referenced from the Root Data Entity via the author property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0000-0002-1825-0097" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": { + "@id": "data.csv" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://orcid.org/0000-0002-1825-0097", + "@type": "Person", + "name": "Josiah Carberry", + "contactPoint": { + "@id": "#josiah-carberry-contact" + }, + "affiliation": { + "@id": "https://ror.org/05f9q8d28" + } + }, + { + "@id": "#josiah-carberry-contact", + "@type": "ContactPoint", + "name": "Josiah Carberry Contact", + "email": "mailto:josiah.carberry@example.com" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example University", + "description": "An example university that the Person entity is affiliated with.", + "url": "https://www.exampleuniversity.edu", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data", + "description": "A sample data file.", + "encodingFormat": "text/csv", + "contentSize": "42" + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example University", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/schema_org_iri_protocol/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/schema_org_iri_protocol/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..e7d17895e --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/schema_org_iri_protocol/invalid/ro-crate-metadata.json @@ -0,0 +1,65 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Schema.org IRI protocol β€” invalid", + "description": "RO-Crate that types an entity with an https Schema.org IRI and references Schema.org terms through https @id values.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": [], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": { + "@id": "https://schema.org/identifier" + }, + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "https://schema.org/Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "additionalType": { + "@id": "https://schema.org/ResearchOrganization" + }, + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/schema_org_iri_protocol/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/schema_org_iri_protocol/valid/ro-crate-metadata.json new file mode 100644 index 000000000..c2ce88add --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/schema_org_iri_protocol/valid/ro-crate-metadata.json @@ -0,0 +1,63 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Schema.org IRI protocol β€” valid", + "description": "RO-Crate whose Schema.org IRIs all use the http namespace that the RO-Crate @context defines.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": [], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "additionalType": { + "@id": "http://schema.org/ResearchOrganization" + }, + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/utf8_identifiers/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/utf8_identifiers/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..d82f4167a --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/utf8_identifiers/invalid/ro-crate-metadata.json @@ -0,0 +1,39 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Percent-encoded UTF-8 β€” invalid", + "description": "RO-Crate where a contextual entity @id uses percent-encoded non-ASCII bytes instead of native UTF-8.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "publisher": {"@id": "#publisher-org"}, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "#donn%C3%A9es-schema", + "@type": "DefinedTerm", + "name": "DonnΓ©es (percent-encoded)", + "description": "A contextual entity whose @id uses %C3%A9 (percent-encoded Γ©) instead of native UTF-8." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/1_metadata_document/utf8_identifiers/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/1_metadata_document/utf8_identifiers/valid/ro-crate-metadata.json new file mode 100644 index 000000000..69db3a8e2 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/1_metadata_document/utf8_identifiers/valid/ro-crate-metadata.json @@ -0,0 +1,79 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "UTF-8 identifiers β€” valid", + "description": "RO-Crate where no @id uses percent-encoded non-ASCII bytes.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": [], + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/invalid/data.csv b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/invalid/data.csv new file mode 100644 index 000000000..53cf763c2 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/invalid/data.csv @@ -0,0 +1,3 @@ +id,value +1,hello +2,world diff --git a/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..8cab89e96 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/invalid/ro-crate-metadata.json @@ -0,0 +1,54 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./x" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./x", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "data.csv" + }, + { + "@id": "ro-crate-preview.html" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data file", + "description": "A sample CSV data file.", + "encodingFormat": "text/csv", + "contentSize": "42" + }, + { + "@id": "ro-crate-preview.html", + "@type": "File", + "name": "RO-Crate Preview", + "description": "HTML preview of the RO-Crate - this SHOULD NOT be in hasPart per RO-Crate 1.3.", + "encodingFormat": "text/html" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/invalid/ro-crate-preview.html b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/invalid/ro-crate-preview.html new file mode 100644 index 000000000..be5c1b2fe --- /dev/null +++ b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/invalid/ro-crate-preview.html @@ -0,0 +1,5 @@ + + +RO-Crate Preview +

Counterexample: preview in hasPart

+ diff --git a/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/valid/data.csv b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/valid/data.csv new file mode 100644 index 000000000..53cf763c2 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/valid/data.csv @@ -0,0 +1,3 @@ +id,value +1,hello +2,world diff --git a/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/valid/ro-crate-metadata.json new file mode 100644 index 000000000..3874cd5b9 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/valid/ro-crate-metadata.json @@ -0,0 +1,146 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Example: preview not in hasPart", + "description": "This RO-Crate correctly omits ro-crate-preview.html from hasPart, following the new RO-Crate 1.3 requirement.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/00k4n6c32" + }, + "hasPart": { + "@id": "data.csv" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "mailto:contact@example.org" + }, + "identifier": { + "@id": "https://ror.org/00k4n6c32" + } + }, + { + "@id": "mailto:contact@example.org", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "contact@example.org", + "name": "Example Research Institute contact point" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data file", + "description": "A sample CSV data file containing temperature readings.", + "encodingFormat": [ + "text/csv", + { + "@id": "https://www.nationalarchives.gov.uk/PRONOM/x-fmt/18" + } + ], + "contentSize": "42", + "contentLocation": { + "@id": "#bologna" + }, + "conformsTo": { + "@id": "#csv-profile" + }, + "dateCreated": "2025-06-15", + "dateModified": "2025-12-01", + "author": { + "@id": "https://orcid.org/0000-0001-2345-6789" + }, + "sdDatePublished": "2026-04-15" + }, + { + "@id": "#csv-profile", + "@type": "CreativeWork", + "name": "CSV Profile for RO-Crate 1.3", + "description": "A profile describing the expected structure and content of CSV files used in RO-Crates.", + "conformsTo": { + "@id": "https://www.iana.org/assignments/media-types/text/csv" + } + }, + { + "@id": "#bologna", + "@type": "Place", + "name": "Bologna, Italy", + "geo": { + "@id": "#bologna-geo" + } + }, + { + "@id": "#bologna-geo", + "@type": [ + "GeoCoordinates", + "Geometry" + ], + "name": "Geographical coordinates of Bologna", + "latitude": 44.4949, + "longitude": 11.3426, + "asWKT": "POINT(11.3426 44.4949)" + }, + { + "@id": "https://orcid.org/0000-0001-2345-6789", + "@type": "Person", + "name": "Dr. Jane Doe", + "affiliation": { + "@id": "https://ror.org/12345" + } + }, + { + "@id": "https://ror.org/12345", + "@type": "Organization", + "name": "Example University", + "url": "https://www.exampleuniversity.edu", + "contactPoint": { + "@id": "mailto:contact@exampleuniversity.edu" + } + }, + { + "@id": "mailto:contact@exampleuniversity.edu", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "contact@exampleuniversity.edu", + "name": "Example University contact point" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/valid/ro-crate-preview.html b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/valid/ro-crate-preview.html new file mode 100644 index 000000000..99f1e3229 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/2_attached_rocrates/attached-preview-not-in-hasPart/valid/ro-crate-preview.html @@ -0,0 +1,8 @@ + + +RO-Crate Preview + +

Example: preview not in hasPart

+

This RO-Crate correctly omits ro-crate-preview.html from hasPart.

+ + diff --git a/tests/data/crates/rocrate-1.3/2_attached_rocrates/non-relative-root-identifier/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/2_attached_rocrates/non-relative-root-identifier/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..6c204ba75 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/2_attached_rocrates/non-relative-root-identifier/invalid/ro-crate-metadata.json @@ -0,0 +1,32 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "invalid-IRI-root-dataset" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "invalid-IRI-root-dataset", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/2_attached_rocrates/non-relative-root-identifier/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/2_attached_rocrates/non-relative-root-identifier/valid/ro-crate-metadata.json new file mode 100644 index 000000000..4472466a8 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/2_attached_rocrates/non-relative-root-identifier/valid/ro-crate-metadata.json @@ -0,0 +1,32 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "https://example.org/external-root-dataset" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "https://example.org/external-root-dataset", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/2_attached_rocrates/relative-root-identifier/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/2_attached_rocrates/relative-root-identifier/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..971661f22 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/2_attached_rocrates/relative-root-identifier/invalid/ro-crate-metadata.json @@ -0,0 +1,32 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./root-dataset" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./root-dataset", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/2_attached_rocrates/relative-root-identifier/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/2_attached_rocrates/relative-root-identifier/valid/ro-crate-metadata.json new file mode 100644 index 000000000..dbed5c3e1 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/2_attached_rocrates/relative-root-identifier/valid/ro-crate-metadata.json @@ -0,0 +1,32 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/3_detached_rocrates/detached-basic/basic-ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/3_detached_rocrates/detached-basic/basic-ro-crate-metadata.json new file mode 100644 index 000000000..4b2339105 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/3_detached_rocrates/detached-basic/basic-ro-crate-metadata.json @@ -0,0 +1,37 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://example.org/ro-crate/detached/basic" + } + }, + { + "@id": "https://example.org/ro-crate/detached/basic", + "@type": "Dataset", + "name": "Detached RO-Crate (basic)", + "description": "Minimal detached RO-Crate referencing a remote file.", + "datePublished": "2024-05-17", + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "hasPart": [ + { + "@id": "https://www.w3.org/TR/PNG/iso_8859-1.txt" + } + ] + }, + { + "@id": "https://www.w3.org/TR/PNG/iso_8859-1.txt", + "@type": "File", + "name": "iso_8859-1.txt", + "description": "Remote text file referenced by the detached RO-Crate.", + "encodingFormat": "text/plain" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/3_detached_rocrates/detached-multi/multi-ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/3_detached_rocrates/detached-multi/multi-ro-crate-metadata.json new file mode 100644 index 000000000..8cc75f87d --- /dev/null +++ b/tests/data/crates/rocrate-1.3/3_detached_rocrates/detached-multi/multi-ro-crate-metadata.json @@ -0,0 +1,60 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://example.org/ro-crate/detached/multi" + } + }, + { + "@id": "https://example.org/ro-crate/detached/multi", + "@type": "Dataset", + "name": "Detached RO-Crate (multi)", + "description": "Detached RO-Crate with multiple remote data entities.", + "datePublished": "2024-05-17", + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "hasPart": [ + { + "@id": "https://www.w3.org/TR/PNG/iso_8859-1.txt" + }, + { + "@id": "https://www.w3.org/2008/site/images/logo-w3c-mobile-lg" + }, + { + "@id": "https://example.org/datasets/sample" + } + ] + }, + { + "@id": "https://www.w3.org/TR/PNG/iso_8859-1.txt", + "@type": "File", + "name": "iso_8859-1.txt", + "description": "Remote text file.", + "encodingFormat": "text/plain" + }, + { + "@id": "https://www.w3.org/2008/site/images/logo-w3c-mobile-lg", + "@type": "File", + "name": "W3C logo", + "description": "Remote image file.", + "encodingFormat": "image/png" + }, + { + "@id": "https://example.org/datasets/sample", + "@type": "Dataset", + "name": "Sample remote dataset", + "description": "Remote dataset referenced by the detached RO-Crate.", + "datePublished": "2024-05-17", + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + } + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/3_detached_rocrates/detached-with-profile/profiled-ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/3_detached_rocrates/detached-with-profile/profiled-ro-crate-metadata.json new file mode 100644 index 000000000..5198cbd0f --- /dev/null +++ b/tests/data/crates/rocrate-1.3/3_detached_rocrates/detached-with-profile/profiled-ro-crate-metadata.json @@ -0,0 +1,51 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://example.org/ro-crate/detached/profiled" + } + }, + { + "@id": "https://example.org/ro-crate/detached/profiled", + "@type": "Dataset", + "name": "Detached RO-Crate (profiled)", + "description": "Detached RO-Crate declaring conformance to an additional profile.", + "datePublished": "2024-05-17", + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "conformsTo": [ + { + "@id": "https://example.org/profiles/example-profile" + } + ], + "hasPart": [ + { + "@id": "https://www.w3.org/TR/PNG/iso_8859-1.txt" + } + ] + }, + { + "@id": "https://example.org/profiles/example-profile", + "@type": [ + "Profile", + "CreativeWork" + ], + "name": "Example profile", + "description": "Example RO-Crate profile contextual entity." + }, + { + "@id": "https://www.w3.org/TR/PNG/iso_8859-1.txt", + "@type": "File", + "name": "iso_8859-1.txt", + "description": "Remote text file.", + "encodingFormat": "text/plain" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/3_detached_rocrates/naming-convention/local-descriptor/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/3_detached_rocrates/naming-convention/local-descriptor/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..84d74e972 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/3_detached_rocrates/naming-convention/local-descriptor/invalid/ro-crate-metadata.json @@ -0,0 +1,37 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://raw.githubusercontent.com/crs4/rocrate-validator/refs/heads/develop/tests/data/crates/valid/workflow-roc/" + } + }, + { + "@id": "https://raw.githubusercontent.com/crs4/rocrate-validator/refs/heads/develop/tests/data/crates/valid/workflow-roc/", + "@type": "Dataset", + "name": "Detached RO-Crate (basic)", + "description": "Minimal detached RO-Crate referencing a remote file.", + "datePublished": "2024-05-17", + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "hasPart": [ + { + "@id": "http://spdx.org/licenses/CC0-1.0" + } + ] + }, + { + "@id": "http://spdx.org/licenses/CC0-1.0", + "@type": "File", + "name": "CC0 1.0 Universal (CC0 1.0) Public Domain Dedication", + "description": "Remote text file referenced by the detached RO-Crate.", + "encodingFormat": "text/plain" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/3_detached_rocrates/naming-convention/local-descriptor/valid/basic-ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/3_detached_rocrates/naming-convention/local-descriptor/valid/basic-ro-crate-metadata.json new file mode 100644 index 000000000..833682604 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/3_detached_rocrates/naming-convention/local-descriptor/valid/basic-ro-crate-metadata.json @@ -0,0 +1,83 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "basic-ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://raw.githubusercontent.com/crs4/rocrate-validator/refs/heads/develop/tests/data/crates/valid/workflow-roc/" + } + }, + { + "@id": "https://raw.githubusercontent.com/crs4/rocrate-validator/refs/heads/develop/tests/data/crates/valid/workflow-roc/", + "@type": "Dataset", + "name": "Detached RO-Crate (basic)", + "description": "Minimal detached RO-Crate with a non-standard metadata descriptor filename.", + "datePublished": "2024-05-17", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "cite-as": { + "@id": "https://raw.githubusercontent.com/crs4/rocrate-validator/refs/heads/develop/tests/data/crates/valid/workflow-roc/" + }, + "url": "https://raw.githubusercontent.com/crs4/rocrate-validator/refs/heads/develop/tests/data/crates/valid/workflow-roc/", + "hasPart": [], + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/3_detached_rocrates/root-data-entity-identifier/online-available/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/3_detached_rocrates/root-data-entity-identifier/online-available/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..06c130c8c --- /dev/null +++ b/tests/data/crates/rocrate-1.3/3_detached_rocrates/root-data-entity-identifier/online-available/invalid/ro-crate-metadata.json @@ -0,0 +1,33 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "about": {"@id": "./"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Online-available RO-Crate with relative root @id (invalid)", + "description": "A detached RO-Crate whose Root Data Entity has a relative @id instead of an absolute URL.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "hasPart": [], + "publisher": {"@id": "https://ror.org/012345678"} + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/3_detached_rocrates/root-data-entity-identifier/online-available/valid/prefix-ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/3_detached_rocrates/root-data-entity-identifier/online-available/valid/prefix-ro-crate-metadata.json new file mode 100644 index 000000000..2ac3d523f --- /dev/null +++ b/tests/data/crates/rocrate-1.3/3_detached_rocrates/root-data-entity-identifier/online-available/valid/prefix-ro-crate-metadata.json @@ -0,0 +1,68 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://example.org/ro-crate/detached/citable-root-data-entity/" + } + }, + { + "@id": "https://example.org/ro-crate/detached/citable-root-data-entity/", + "@type": "Dataset", + "name": "Detached RO-Crate with absolute root @id", + "description": "A detached RO-Crate whose Root Data Entity has an absolute URL as @id.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "cite-as": { + "@id": "https://example.org/ro-crate/detached/citable-root-data-entity/" + }, + "url": "https://example.org/ro-crate/detached/citable-root-data-entity/", + "publisher": { + "@id": "https://ror.org/012345678" + }, + "funder": { + "@id": "https://ror.org/012345678" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "mailto:contact@example.org" + } + }, + { + "@id": "mailto:contact@example.org", + "@type": "ContactPoint", + "contactType": "customer service", + "email": "contact@example.org", + "name": "Example Research Institute contact point" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/5_metadata_entities/entity_reachability/invalid/my-data-file.txt b/tests/data/crates/rocrate-1.3/5_metadata_entities/entity_reachability/invalid/my-data-file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/5_metadata_entities/entity_reachability/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/5_metadata_entities/entity_reachability/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..76597ad63 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/5_metadata_entities/entity_reachability/invalid/ro-crate-metadata.json @@ -0,0 +1,38 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "my-data-file.txt", + "@type": "File", + "name": "My Data File", + "description": "A data file that is part of the RO-Crate." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/5_metadata_entities/entity_reachability/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/5_metadata_entities/entity_reachability/valid/ro-crate-metadata.json new file mode 100644 index 000000000..af97f0a3b --- /dev/null +++ b/tests/data/crates/rocrate-1.3/5_metadata_entities/entity_reachability/valid/ro-crate-metadata.json @@ -0,0 +1,79 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/invalid/my-data-file.txt b/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/invalid/my-data-file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..eb470334d --- /dev/null +++ b/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/invalid/ro-crate-metadata.json @@ -0,0 +1,41 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "my-data-file.txt" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "my-data-file.txt", + "@type": "File", + "description": "A data file that is part of the RO-Crate." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/valid/my-data-file.txt b/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/valid/my-data-file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/valid/ro-crate-metadata.json new file mode 100644 index 000000000..e32a443b5 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_name/valid/ro-crate-metadata.json @@ -0,0 +1,79 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": { + "@id": "my-data-file.txt" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + } + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "my-data-file.txt", + "@type": "File", + "name": "My Data File", + "description": "A data file that is part of the RO-Crate.", + "encodingFormat": "text/plain", + "contentSize": "0" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_schema_type/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_schema_type/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..dcec571d7 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_schema_type/invalid/ro-crate-metadata.json @@ -0,0 +1,32 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "http://purl.org/dc/terms/LicenseDocument", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_schema_type/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_schema_type/valid/ro-crate-metadata.json new file mode 100644 index 000000000..af97f0a3b --- /dev/null +++ b/tests/data/crates/rocrate-1.3/5_metadata_entities/recommended_schema_type/valid/ro-crate-metadata.json @@ -0,0 +1,79 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/recommended_prefix/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/recommended_prefix/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..8707b6e66 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/recommended_prefix/invalid/ro-crate-metadata.json @@ -0,0 +1,32 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://xxxw3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/recommended_prefix/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/recommended_prefix/valid/ro-crate-metadata.json new file mode 100644 index 000000000..af97f0a3b --- /dev/null +++ b/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/recommended_prefix/valid/ro-crate-metadata.json @@ -0,0 +1,79 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/single_value/invalid/my-data-file.txt b/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/single_value/invalid/my-data-file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/single_value/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/single_value/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..c75e3be7b --- /dev/null +++ b/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/single_value/invalid/ro-crate-metadata.json @@ -0,0 +1,37 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate/1.3" + }, + { + "@id": "https://w3id.org/ro/crate/1.1" + } + ] + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/single_value/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/single_value/valid/ro-crate-metadata.json new file mode 100644 index 000000000..af97f0a3b --- /dev/null +++ b/tests/data/crates/rocrate-1.3/6_metadata_descriptor/recommended_conformsTo/single_value/valid/ro-crate-metadata.json @@ -0,0 +1,79 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/additional_conformsTo/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/additional_conformsTo/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..1b84b6bfc --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/additional_conformsTo/invalid/ro-crate-metadata.json @@ -0,0 +1,55 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "cite-as": { + "@id": "https://doi.org/10.1234/example-doi" + }, + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate/1.1x" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://doi.org/10.1234/example-doi", + "@type": "CreativeWork", + "name": "Example DOI Citation", + "description": "An example citation for the Root Data Entity using a DOI.", + "url": "https://example.com/download/ro-crate.zip" + }, + { + "@id": "https://w3id.org/ro/crate/1.1x", + "@type": [ + "CreativeWork" + ], + "name": "RO-Crate 1.1x Profile", + "description": "An example of an additional `conformsTo` profile reference in the Root Data Entity metadata." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/additional_conformsTo/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/additional_conformsTo/valid/ro-crate-metadata.json new file mode 100644 index 000000000..c2e300e85 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/additional_conformsTo/valid/ro-crate-metadata.json @@ -0,0 +1,56 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "cite-as": { + "@id": "https://doi.org/10.1234/example-doi" + }, + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate/1.1x" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://doi.org/10.1234/example-doi", + "@type": "CreativeWork", + "name": "Example DOI Citation", + "description": "An example citation for the Root Data Entity using a DOI.", + "url": "https://example.com/download/ro-crate.zip" + }, + { + "@id": "https://w3id.org/ro/crate/1.1x", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "RO-Crate 1.1x Profile", + "description": "An example of an additional `conformsTo` profile reference in the Root Data Entity metadata." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_citeas_for_resolvable_id/invalid/citeas-ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_citeas_for_resolvable_id/invalid/citeas-ro-crate-metadata.json new file mode 100644 index 000000000..f2e6868d8 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_citeas_for_resolvable_id/invalid/citeas-ro-crate-metadata.json @@ -0,0 +1,36 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "https://example.org/ro-crate/detached/citable-root-data-entity/" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "https://example.org/ro-crate/detached/citable-root-data-entity/", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "url": "https://example.org/ro-crate/detached/citable-root-data-entity/", + "distribution": { + "@id": "https://example.org/ro-crate/detached/citable-root-data-entity/" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_citeas_for_resolvable_id/valid/citeas-ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_citeas_for_resolvable_id/valid/citeas-ro-crate-metadata.json new file mode 100644 index 000000000..a0f794606 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_citeas_for_resolvable_id/valid/citeas-ro-crate-metadata.json @@ -0,0 +1,83 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "https://example.org/ro-crate/detached/citable-root-data-entity/" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "https://example.org/ro-crate/detached/citable-root-data-entity/", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "cite-as": { + "@id": "https://example.org/ro-crate/detached/citable-root-data-entity/" + }, + "url": "https://example.org/ro-crate/detached/citable-root-data-entity/", + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_conformsto_if_profiles/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_conformsto_if_profiles/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..f8b7c6e70 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_conformsto_if_profiles/invalid/ro-crate-metadata.json @@ -0,0 +1,74 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": ["CreativeWork", "schema:Book"], + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: conformsTo missing despite profile in graph", + "description": "This RO-Crate has a Profile entity in the graph but Root lacks conformsTo", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "#publisher-org" + }, + "funder": { + "@id": "#funder-org" + }, + "identifier": { + "@id": "#identifier-pv" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license" + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "#funder-org", + "@type": "Organization", + "name": "European Commission", + "url": "https://ec.europa.eu", + "funder": { + "@id": "https://ror.org/00k4n6c32" + } + }, + { + "@id": "https://w3id.org/ro/crate/1.3/profile1", + "@type": "Profile", + "name": "Example Profile", + "url": "https://w3id.org/ro/crate/1.3/profile1" + }, + { + "@id": "#identifier-pv", + "@type": "PropertyValue", + "name": "Persistent identifier", + "value": "10.5281/zenodo.1234567" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "External Funder", + "url": "https://ror.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_conformsto_if_profiles/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_conformsto_if_profiles/valid/ro-crate-metadata.json new file mode 100644 index 000000000..860d0717f --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_conformsto_if_profiles/valid/ro-crate-metadata.json @@ -0,0 +1,112 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": [ + "CreativeWork", + "schema:Book" + ], + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: conformsTo present with profile reference", + "description": "This RO-Crate conforms to a profile and has conformsTo on Root", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": [ + { + "@id": "https://ror.org/00qj9e848" + }, + { + "@id": "https://ror.org/00k4n6c32" + } + ], + "identifier": { + "@id": "#identifier-pv" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3/profile1" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00qj9e848", + "@type": "Organization", + "name": "European Commission", + "url": "https://ec.europa.eu", + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "contactPoint": { + "@id": "#contact-point-10" + } + }, + { + "@id": "https://w3id.org/ro/crate/1.3/profile1", + "@type": "Profile", + "name": "Example Profile", + "url": "https://w3id.org/ro/crate/1.3/profile1" + }, + { + "@id": "#identifier-pv", + "@type": "PropertyValue", + "name": "Persistent identifier", + "value": "10.5281/zenodo.1234567" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "External Funder", + "url": "https://ror.org", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for External Funder", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-10", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_contactpoint/invalid_missing/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_contactpoint/invalid_missing/ro-crate-metadata.json new file mode 100644 index 000000000..1b3520029 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_contactpoint/invalid_missing/ro-crate-metadata.json @@ -0,0 +1,63 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Root contactPoint missing — invalid", + "description": "RO-Crate whose Root Data Entity omits the `contactPoint` property entirely.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": [], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#org-contact" + } + }, + { + "@id": "#org-contact", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_contactpoint/invalid_wrong_type/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_contactpoint/invalid_wrong_type/ro-crate-metadata.json new file mode 100644 index 000000000..dabe41800 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_contactpoint/invalid_wrong_type/ro-crate-metadata.json @@ -0,0 +1,71 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Root contactPoint wrong type — invalid", + "description": "RO-Crate whose Root Data Entity references a contactPoint that is NOT a ContactPoint contextual entity (it's a plain Thing).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "contactPoint": { + "@id": "#root-contact" + }, + "hasPart": [], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#org-contact" + } + }, + { + "@id": "#org-contact", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#root-contact", + "@type": "Thing", + "name": "Not actually a ContactPoint" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_contactpoint/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_contactpoint/valid/ro-crate-metadata.json new file mode 100644 index 000000000..cb1f46ea2 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_contactpoint/valid/ro-crate-metadata.json @@ -0,0 +1,73 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Root contactPoint present — valid", + "description": "RO-Crate whose Root Data Entity declares a contactPoint referencing a ContactPoint contextual entity.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "contactPoint": { + "@id": "#root-contact" + }, + "hasPart": [], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#org-contact" + } + }, + { + "@id": "#org-contact", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#root-contact", + "@type": "ContactPoint", + "name": "RO-Crate primary contact", + "email": "mailto:crate-contact@example.org", + "contactType": "customer support" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_datepublished_day_precision/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_datepublished_day_precision/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..6656979d9 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_datepublished_day_precision/invalid/ro-crate-metadata.json @@ -0,0 +1,68 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": ["CreativeWork", "schema:Book"], + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: datePublished with year only", + "description": "This RO-Crate has a datePublished property specified to year only (not day precision)", + "datePublished": "2024", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "#publisher-org" + }, + "funder": { + "@id": "#funder-org" + }, + "identifier": { + "@id": "#identifier-pv" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license" + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "#funder-org", + "@type": "Organization", + "name": "European Commission", + "url": "https://ec.europa.eu", + "funder": { + "@id": "https://ror.org/00k4n6c32" + } + }, + { + "@id": "#identifier-pv", + "@type": "PropertyValue", + "name": "Persistent identifier", + "value": "10.5281/zenodo.1234567" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "External Funder", + "url": "https://ror.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_datepublished_day_precision/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_datepublished_day_precision/valid/ro-crate-metadata.json new file mode 100644 index 000000000..9d707e224 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_datepublished_day_precision/valid/ro-crate-metadata.json @@ -0,0 +1,103 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": [ + "CreativeWork", + "schema:Book" + ], + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: datePublished with day precision", + "description": "This RO-Crate has a datePublished property specified to day precision (YYYY-MM-DD)", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": [ + { + "@id": "https://ror.org/00qj9e848" + }, + { + "@id": "https://ror.org/00k4n6c32" + } + ], + "identifier": { + "@id": "#identifier-pv" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00qj9e848", + "@type": "Organization", + "name": "European Commission", + "url": "https://ec.europa.eu", + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "contactPoint": { + "@id": "#contact-point-10" + } + }, + { + "@id": "#identifier-pv", + "@type": "PropertyValue", + "name": "Persistent identifier", + "value": "10.5281/zenodo.1234567" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "External Funder", + "url": "https://ror.org", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for External Funder", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-10", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/invalid_no_funder/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/invalid_no_funder/ro-crate-metadata.json new file mode 100644 index 000000000..653437989 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/invalid_no_funder/ro-crate-metadata.json @@ -0,0 +1,40 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Funded RO-Crate — missing funder", + "description": "RO-Crate that describes a research project (Grant entity) but whose Root Data Entity has no `funder` property, triggering the conditional RECOMMENDED funder check.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "publisher": {"@id": "#publisher-org"}, + "mentions": {"@id": "#grant-eu-2024"}, + "hasPart": [] + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "#grant-eu-2024", + "@type": "Grant", + "name": "EU Horizon 2024 Grant", + "description": "A research grant referenced in the crate; the Root SHOULD declare the corresponding funder." + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/invalid_no_project_funder/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/invalid_no_project_funder/ro-crate-metadata.json new file mode 100644 index 000000000..8f45a5d02 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/invalid_no_project_funder/ro-crate-metadata.json @@ -0,0 +1,41 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Funded RO-Crate — project org missing funder", + "description": "RO-Crate whose project Organization is referenced as funder from Root but does not itself reference an external funder.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "publisher": {"@id": "#publisher-org"}, + "funder": {"@id": "#project-org"}, + "hasPart": [] + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "#project-org", + "@type": "Organization", + "name": "Example Research Project", + "description": "A project Organization that does not reference any external funder.", + "url": "https://example.org/projects/example-project" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/invalid_non_org_funder/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/invalid_non_org_funder/ro-crate-metadata.json new file mode 100644 index 000000000..adac51fdd --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/invalid_non_org_funder/ro-crate-metadata.json @@ -0,0 +1,40 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Funded RO-Crate — funder is not Organization", + "description": "RO-Crate whose Root Data Entity references a funder that is a Person, not an Organization.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "publisher": {"@id": "#publisher-org"}, + "funder": {"@id": "#funder-person"}, + "hasPart": [] + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "#funder-person", + "@type": "Person", + "name": "Jane Doe", + "description": "An individual acting as funder — should be Organization instead." + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/valid/ro-crate-metadata.json new file mode 100644 index 000000000..ad04789a5 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_funding/valid/ro-crate-metadata.json @@ -0,0 +1,103 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Funded RO-Crate — valid", + "description": "RO-Crate whose Root Data Entity references both a project Organization and an external funder directly via `funder`.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": [ + { + "@id": "https://ror.org/00k4n6c32" + }, + { + "@id": "https://ror.org/00qj9e848" + } + ], + "hasPart": [], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00qj9e848", + "@type": "Organization", + "name": "Example Research Project", + "description": "The research project associated with this RO-Crate.", + "url": "https://example.org/projects/example-project", + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "contactPoint": { + "@id": "#contact-point-10" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-10", + "@type": "ContactPoint", + "name": "Contact for Example Research Project", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_if_pid/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_if_pid/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..e25a7b019 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_if_pid/invalid/ro-crate-metadata.json @@ -0,0 +1,59 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": ["CreativeWork", "schema:Book"], + "about": { + "@id": "https://example.org/crate1" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "https://example.org/crate1", + "@type": "Dataset", + "name": "Test RO-Crate: identifier missing with absolute URI", + "description": "This RO-Crate has an absolute URI as @id but does NOT include an identifier property", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "#publisher-org" + }, + "funder": { + "@id": "#funder-org" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license" + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "#funder-org", + "@type": "Organization", + "name": "European Commission", + "url": "https://ec.europa.eu", + "funder": { + "@id": "https://ror.org/00k4n6c32" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "External Funder", + "url": "https://ror.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_if_pid/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_if_pid/valid/ro-crate-metadata.json new file mode 100644 index 000000000..a554c1562 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_if_pid/valid/ro-crate-metadata.json @@ -0,0 +1,103 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": [ + "CreativeWork", + "schema:Book" + ], + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: identifier present with absolute URI", + "description": "This RO-Crate has an absolute URI as @id and includes an identifier property", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": [ + { + "@id": "https://ror.org/00qj9e848" + }, + { + "@id": "https://ror.org/00k4n6c32" + } + ], + "identifier": { + "@id": "#identifier-pv" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00qj9e848", + "@type": "Organization", + "name": "European Commission", + "url": "https://ec.europa.eu", + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "contactPoint": { + "@id": "#contact-point-10" + } + }, + { + "@id": "#identifier-pv", + "@type": "PropertyValue", + "name": "Persistent identifier", + "value": "10.5281/zenodo.1234567" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "External Funder", + "url": "https://ror.org", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for External Funder", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-10", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_propertyvalue/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_propertyvalue/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..8f0e4e11f --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_propertyvalue/invalid/ro-crate-metadata.json @@ -0,0 +1,60 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": ["CreativeWork", "schema:Book"], + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: identifier does NOT use PropertyValue", + "description": "This RO-Crate has identifier as a plain string instead of PropertyValue", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "#publisher-org" + }, + "funder": { + "@id": "#funder-org" + }, + "identifier": "10.5281/zenodo.1234567", + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license" + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "#funder-org", + "@type": "Organization", + "name": "European Commission", + "url": "https://ec.europa.eu", + "funder": { + "@id": "https://ror.org/00k4n6c32" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "External Funder", + "url": "https://ror.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_propertyvalue/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_propertyvalue/valid/ro-crate-metadata.json new file mode 100644 index 000000000..ec6f37961 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_propertyvalue/valid/ro-crate-metadata.json @@ -0,0 +1,103 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": [ + "CreativeWork", + "schema:Book" + ], + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: identifier uses PropertyValue approach", + "description": "This RO-Crate uses PropertyValue for identifier as per Science On Schema.org", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": [ + { + "@id": "https://ror.org/00qj9e848" + }, + { + "@id": "https://ror.org/00k4n6c32" + } + ], + "identifier": { + "@id": "#identifier-pv" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00qj9e848", + "@type": "Organization", + "name": "European Commission", + "url": "https://ec.europa.eu", + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "contactPoint": { + "@id": "#contact-point-10" + } + }, + { + "@id": "#identifier-pv", + "@type": "PropertyValue", + "name": "Persistent identifier", + "value": "10.5281/zenodo.1234567" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "External Funder", + "url": "https://ror.org", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for External Funder", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-10", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_resolution/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_resolution/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..72bab9215 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_resolution/invalid/ro-crate-metadata.json @@ -0,0 +1,27 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Root identifier resolution — invalid", + "description": "RO-Crate whose Root Data Entity has an identifier URL that does not resolve to RO-Crate content (returns HTML landing page).", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "hasPart": [], + "identifier": "https://doi.org/10.1234/unresolvable-rocrate" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_resolution/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_resolution/valid/ro-crate-metadata.json new file mode 100644 index 000000000..13449e141 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_identifier_resolution/valid/ro-crate-metadata.json @@ -0,0 +1,79 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Root identifier resolution — valid", + "description": "RO-Crate whose Root Data Entity has an identifier URL that resolves to RO-Crate content via Signposting.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "identifier": { + "@id": "#identifier-pv" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + } + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + }, + { + "@id": "#identifier-pv", + "@type": "PropertyValue", + "name": "DOI", + "propertyID": "https://registry.identifiers.org/registry/doi", + "value": "https://doi.org/10.1234/resolvable-rocrate" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_publisher/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_publisher/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..cf13d2561 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_publisher/invalid/ro-crate-metadata.json @@ -0,0 +1,26 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Publisher missing — invalid", + "description": "RO-Crate whose Root Data Entity does not declare a publisher.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_publisher/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_publisher/valid/ro-crate-metadata.json new file mode 100644 index 000000000..1aabf41b4 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_publisher/valid/ro-crate-metadata.json @@ -0,0 +1,79 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Publisher present — valid", + "description": "RO-Crate whose Root Data Entity declares a publisher Organization.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": [], + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_reference_type_for_licence/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_reference_type_for_licence/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..ee02699a1 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_reference_type_for_licence/invalid/ro-crate-metadata.json @@ -0,0 +1,31 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_reference_type_for_licence/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_reference_type_for_licence/valid/ro-crate-metadata.json new file mode 100644 index 000000000..dbed5c3e1 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/recommended_reference_type_for_licence/valid/ro-crate-metadata.json @@ -0,0 +1,32 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_datePublished/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_datePublished/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..ee02699a1 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_datePublished/invalid/ro-crate-metadata.json @@ -0,0 +1,31 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_datePublished/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_datePublished/valid/ro-crate-metadata.json new file mode 100644 index 000000000..dbed5c3e1 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_datePublished/valid/ro-crate-metadata.json @@ -0,0 +1,32 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_downloadable_citeas/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_downloadable_citeas/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..4f8391888 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_downloadable_citeas/invalid/ro-crate-metadata.json @@ -0,0 +1,41 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "cite-as": { + "@id": "https://doi.org/10.1234/example-doi" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://doi.org/10.1234/example-doi", + "@type": "CreativeWork", + "name": "Example DOI Citation", + "description": "An example citation for the Root Data Entity using a DOI." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_downloadable_citeas/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_downloadable_citeas/valid/ro-crate-metadata.json new file mode 100644 index 000000000..0da8d7b11 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_downloadable_citeas/valid/ro-crate-metadata.json @@ -0,0 +1,42 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [], + "cite-as": { + "@id": "https://doi.org/10.1234/example-doi" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://doi.org/10.1234/example-doi", + "@type": "CreativeWork", + "name": "Example DOI Citation", + "description": "An example citation for the Root Data Entity using a DOI.", + "url": "https://example.com/download/ro-crate.zip" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid/file1.txt b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid/file1.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..5ac9e2a94 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid/ro-crate-metadata.json @@ -0,0 +1,64 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: hasPart does NOT reference all Data Entities", + "description": "This RO-Crate has hasPart that is missing reference to file2.txt", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "#publisher-org" + }, + "funder": { + "@id": "#funder-org" + }, + "hasPart": [ + { "@id": "file1.txt" } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International" + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "#funder-org", + "@type": "Organization", + "name": "European Commission", + "url": "https://ec.europa.eu", + "funder": { + "@id": "https://ror.org/00k4n6c32" + } + }, + { + "@id": "file1.txt", + "@type": "File", + "name": "File 1" + }, + { + "@id": "file2.txt", + "@type": "File", + "name": "File 2" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_dataset_not_in_haspart/file1.txt b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_dataset_not_in_haspart/file1.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_dataset_not_in_haspart/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_dataset_not_in_haspart/ro-crate-metadata.json new file mode 100644 index 000000000..67f8bdca2 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_dataset_not_in_haspart/ro-crate-metadata.json @@ -0,0 +1,44 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: Sub-Dataset not in hasPart", + "description": "This RO-Crate has a sub-Dataset (Directory Data Entity) that is missing from hasPart.", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { "@id": "file1.txt" } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International" + }, + { + "@id": "file1.txt", + "@type": "File", + "name": "File 1" + }, + { + "@id": "subdir/", + "@type": "Dataset", + "name": "Sub-directory Data Entity", + "description": "A sub-Dataset Data Entity that is not referenced in hasPart." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_web_entity_not_in_haspart/local.txt b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_web_entity_not_in_haspart/local.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_web_entity_not_in_haspart/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_web_entity_not_in_haspart/ro-crate-metadata.json new file mode 100644 index 000000000..2bb7ae6ca --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_web_entity_not_in_haspart/ro-crate-metadata.json @@ -0,0 +1,44 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: Web Data Entity not in hasPart", + "description": "This RO-Crate has a web-based File entity (absolute URL @id) that is missing from hasPart.", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { "@id": "local.txt" } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International" + }, + { + "@id": "local.txt", + "@type": "File", + "name": "Local file" + }, + { + "@id": "https://example.org/data/remote.tsv", + "@type": "File", + "name": "Remote web-based data file", + "encodingFormat": "text/tab-separated-values" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_workflow_not_in_haspart/data.txt b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_workflow_not_in_haspart/data.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_workflow_not_in_haspart/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_workflow_not_in_haspart/ro-crate-metadata.json new file mode 100644 index 000000000..1cb502a20 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_workflow_not_in_haspart/ro-crate-metadata.json @@ -0,0 +1,43 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: Workflow not in hasPart", + "description": "This RO-Crate has a workflow File entity (File/SoftwareSourceCode/ComputationalWorkflow) that is missing from hasPart.", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { "@id": "data.txt" } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International" + }, + { + "@id": "data.txt", + "@type": "File", + "name": "Data File" + }, + { + "@id": "workflow.ga", + "@type": ["File", "SoftwareSourceCode", "ComputationalWorkflow"], + "name": "My Workflow" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_workflow_not_in_haspart/workflow.ga b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/invalid_workflow_not_in_haspart/workflow.ga new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/profile_root_contextual_datasets/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/profile_root_contextual_datasets/ro-crate-metadata.json new file mode 100644 index 000000000..1942090cc --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/profile_root_contextual_datasets/ro-crate-metadata.json @@ -0,0 +1,67 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "https://w3id.org/ro/crate/1.3", + "@type": [ + "Dataset", + "Profile" + ], + "name": "Test RO-Crate Profile", + "description": "A Profile Crate with contextual Dataset references outside hasPart.", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "isBasedOn": { + "@id": "https://w3id.org/ro/crate/1.2" + }, + "citation": { + "@id": "https://doi.org/10.3233/DS-210053" + }, + "hasPart": [ + { + "@id": "https://example.org/profile-document" + } + ] + }, + { + "@id": "https://w3id.org/ro/crate/1.2", + "@type": "Dataset", + "name": "RO-Crate specification 1.2" + }, + { + "@id": "https://doi.org/10.3233/DS-210053", + "@type": "ScholarlyArticle", + "name": "Example citation", + "mainEntityOfPage": { + "@id": "https://example.org/referenced-dataset" + } + }, + { + "@id": "https://example.org/referenced-dataset", + "@type": "Dataset", + "name": "Referenced dataset" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International" + }, + { + "@id": "https://example.org/profile-document", + "@type": "File", + "name": "Profile document" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/valid/file1.txt b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/valid/file1.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/valid/file2.txt b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/valid/file2.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/valid/ro-crate-metadata.json new file mode 100644 index 000000000..11f1b9203 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/7_root_data_entity/required_haspart_all_data_entities/valid/ro-crate-metadata.json @@ -0,0 +1,65 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test RO-Crate: hasPart references all Data Entities", + "description": "This RO-Crate has hasPart that references all Data Entities in the graph", + "datePublished": "2024-01-15", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "#publisher-org" + }, + "funder": { + "@id": "#funder-org" + }, + "hasPart": [ + { "@id": "file1.txt" }, + { "@id": "file2.txt" } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International" + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "#funder-org", + "@type": "Organization", + "name": "European Commission", + "url": "https://ec.europa.eu", + "funder": { + "@id": "https://ror.org/00k4n6c32" + } + }, + { + "@id": "file1.txt", + "@type": "File", + "name": "File 1" + }, + { + "@id": "file2.txt", + "@type": "File", + "name": "File 2" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/invalid/data/research-data.csv b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/invalid/data/research-data.csv new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..6e0292d9c --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/invalid/ro-crate-metadata.json @@ -0,0 +1,80 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test crate with redundant license on Data Entity", + "description": "The Data Entity repeats the same license as the Root.", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "datePublished": "2024-01-01", + "publisher": { + "@id": "https://ror.org/012345678" + }, + "hasPart": { + "@id": "data/research-data.csv" + }, + "funder": { + "@id": "https://ror.org/012345678" + }, + "cite-as": { + "@id": "./" + }, + "url": "./", + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "data/research-data.csv", + "@type": "File", + "name": "research-data.csv", + "description": "Research data under the same license as the Root.", + "encodingFormat": "text/csv", + "contentSize": "2048", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + } + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/valid/data/open-data.csv b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/valid/data/open-data.csv new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/valid/ro-crate-metadata.json new file mode 100644 index 000000000..d63b769e4 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/data_entity_license/valid/ro-crate-metadata.json @@ -0,0 +1,86 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test crate with divergent licenses", + "description": "The Root Data Entity has CC-BY-4.0; a Data Entity has CC0.", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "datePublished": "2024-01-01", + "publisher": { + "@id": "https://ror.org/012345678" + }, + "hasPart": { + "@id": "data/open-data.csv" + }, + "funder": { + "@id": "https://ror.org/012345678" + }, + "cite-as": { + "@id": "./" + }, + "url": "./", + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "data/open-data.csv", + "@type": "File", + "name": "open-data.csv", + "description": "Open data file under CC0.", + "encodingFormat": "text/csv", + "contentSize": "1024", + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + } + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "http://spdx.org/licenses/CC0-1.0", + "@type": "CreativeWork", + "name": "CC0 1.0 Universal", + "description": "Public domain dedication." + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_has_part/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_has_part/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..c0827e2d1 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_has_part/invalid/ro-crate-metadata.json @@ -0,0 +1,55 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Dataset hasPart — invalid", + "description": "RO-Crate where the local Dataset sub-entity does NOT declare hasPart.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "publisher": {"@id": "#publisher-org"}, + "funder": {"@id": "https://ror.org/00k4n6c32"}, + "identifier": {"@id": "#uuid"}, + "hasPart": [{"@id": "subdir/"}] + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "subdir/", + "@type": "Dataset", + "name": "Sub-directory without hasPart", + "description": "A local Dataset sub-entity that omits hasPart, violating the RECOMMENDED convention." + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_has_part/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_has_part/valid/ro-crate-metadata.json new file mode 100644 index 000000000..482d981f5 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_has_part/valid/ro-crate-metadata.json @@ -0,0 +1,98 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Dataset hasPart — valid", + "description": "RO-Crate where the local Dataset sub-entity declares hasPart listing its contents.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + }, + "hasPart": { + "@id": "subdir/" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "subdir/", + "@type": "Dataset", + "name": "Sub-directory with hasPart", + "description": "A local Dataset sub-entity that correctly lists its contents via hasPart.", + "hasPart": { + "@id": "subdir/data.csv" + } + }, + { + "@id": "subdir/data.csv", + "@type": "File", + "name": "Data file", + "description": "A data file listed via hasPart.", + "encodingFormat": "text/csv", + "contentSize": "1024" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_trailing_slash/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_trailing_slash/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..caae9e514 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_trailing_slash/invalid/ro-crate-metadata.json @@ -0,0 +1,64 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Dataset trailing slash — invalid", + "description": "RO-Crate where the local Dataset sub-entity @id does NOT end with '/'.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "publisher": {"@id": "#publisher-org"}, + "funder": {"@id": "https://ror.org/00k4n6c32"}, + "identifier": {"@id": "#uuid"}, + "hasPart": [{"@id": "subdir"}] + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "subdir", + "@type": "Dataset", + "name": "Sub-directory (missing trailing slash)", + "description": "A local Dataset sub-entity whose @id does NOT end with '/'. This violates the RECOMMENDED convention.", + "hasPart": [{"@id": "subdir/data.csv"}] + }, + { + "@id": "subdir/data.csv", + "@type": "File", + "name": "Data file", + "description": "A data file inside the sub-directory.", + "encodingFormat": "text/csv", + "contentSize": "1024" + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_trailing_slash/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_trailing_slash/valid/ro-crate-metadata.json new file mode 100644 index 000000000..8db8d4808 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/dataset_trailing_slash/valid/ro-crate-metadata.json @@ -0,0 +1,98 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Dataset trailing slash — valid", + "description": "RO-Crate where the local Dataset sub-entity @id ends with '/' as required.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + }, + "hasPart": { + "@id": "subdir/" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "subdir/", + "@type": "Dataset", + "name": "Sub-directory", + "description": "A local Dataset sub-entity whose @id correctly ends with '/'.", + "hasPart": { + "@id": "subdir/data.csv" + } + }, + { + "@id": "subdir/data.csv", + "@type": "File", + "name": "Data file", + "description": "A data file inside the sub-directory.", + "encodingFormat": "text/csv", + "contentSize": "1024" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/detached_rocrate_dataEntities/invalid/basic-ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/detached_rocrate_dataEntities/invalid/basic-ro-crate-metadata.json new file mode 100644 index 000000000..a94448efb --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/detached_rocrate_dataEntities/invalid/basic-ro-crate-metadata.json @@ -0,0 +1,37 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://example.org/ro-crate/detached/basic" + } + }, + { + "@id": "https://example.org/ro-crate/detached/basic", + "@type": "Dataset", + "name": "Detached RO-Crate (basic)", + "description": "Minimal detached RO-Crate referencing a remote file.", + "datePublished": "2024-05-17", + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "hasPart": [ + { + "@id": "./my-test-file.txt" + } + ] + }, + { + "@id": "./my-test-file.txt", + "@type": "File", + "name": "iso_8859-1.txt", + "description": "Remote text file referenced by the detached RO-Crate.", + "encodingFormat": "text/plain" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/detached_rocrate_dataEntities/valid/basic-ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/detached_rocrate_dataEntities/valid/basic-ro-crate-metadata.json new file mode 100644 index 000000000..4b2339105 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/detached_rocrate_dataEntities/valid/basic-ro-crate-metadata.json @@ -0,0 +1,37 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://example.org/ro-crate/detached/basic" + } + }, + { + "@id": "https://example.org/ro-crate/detached/basic", + "@type": "Dataset", + "name": "Detached RO-Crate (basic)", + "description": "Minimal detached RO-Crate referencing a remote file.", + "datePublished": "2024-05-17", + "license": { + "@id": "http://spdx.org/licenses/CC0-1.0" + }, + "hasPart": [ + { + "@id": "https://www.w3.org/TR/PNG/iso_8859-1.txt" + } + ] + }, + { + "@id": "https://www.w3.org/TR/PNG/iso_8859-1.txt", + "@type": "File", + "name": "iso_8859-1.txt", + "description": "Remote text file referenced by the detached RO-Crate.", + "encodingFormat": "text/plain" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..67e894e40 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/invalid/ro-crate-metadata.json @@ -0,0 +1,46 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "/my-test-file.txt" + } + ] + }, + { + "@id": "/my-test-file.txt", + "@type": "File", + "name": "My Test File", + "description": "A test file for RO-Crate.", + "encodingFormat": "text/plain", + "contentSize": 1024, + "dateCreated": "2024-01-01T12:00:00Z", + "dateModified": "2024-01-02T12:00:00Z" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/valid/my-test-file-2.txt b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/valid/my-test-file-2.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/valid/my-test-file.txt b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/valid/my-test-file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/valid/ro-crate-metadata.json new file mode 100644 index 000000000..81c187a35 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/local_entity_reference/valid/ro-crate-metadata.json @@ -0,0 +1,59 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "./my-test-file.txt" + }, + { + "@id": "my-test-file-2.txt" + } + ] + }, + { + "@id": "./my-test-file.txt", + "@type": "File", + "name": "My Test File", + "description": "A test file for RO-Crate.", + "encodingFormat": "text/plain", + "contentSize": 1024, + "dateCreated": "2024-01-01T12:00:00Z", + "dateModified": "2024-01-02T12:00:00Z" + }, + { + "@id": "my-test-file-2.txt", + "@type": "File", + "name": "My Test File 2", + "description": "Another test file for RO-Crate.", + "encodingFormat": "text/plain", + "contentSize": 2048, + "dateCreated": "2024-01-01T12:00:00Z", + "dateModified": "2024-01-02T12:00:00Z" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/missing_file_local_path/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/missing_file_local_path/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..5a0d139e0 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/missing_file_local_path/invalid/ro-crate-metadata.json @@ -0,0 +1,47 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "about": {"@id": "./"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test crate missing file without localPath", + "description": "A local file is referenced but not present in the payload and has no localPath.", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "datePublished": "2024-01-01", + "publisher": {"@id": "https://ror.org/012345678"}, + "hasPart": [{"@id": "data/missing-file.csv"}, {"@id": "#output-file.csv"}] + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "data/missing-file.csv", + "@type": "File", + "name": "missing-file.csv", + "description": "A CSV file that is not included in the payload and has no localPath.", + "encodingFormat": "text/csv" + }, + { + "@id": "#output-file.csv", + "@type": "File", + "name": "output-file.csv", + "description": "A deliberately absent file without localPath.", + "encodingFormat": "text/csv" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/missing_file_local_path/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/missing_file_local_path/valid/ro-crate-metadata.json new file mode 100644 index 000000000..45a22932c --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/missing_file_local_path/valid/ro-crate-metadata.json @@ -0,0 +1,88 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Test crate with localPath for missing file", + "description": "A local file is referenced but not present in the payload; localPath is provided.", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "datePublished": "2024-01-01", + "publisher": { + "@id": "https://ror.org/012345678" + }, + "funder": { + "@id": "https://ror.org/012345678" + }, + "hasPart": [ + { + "@id": "data/missing-file.csv" + }, + { + "@id": "#output-file.csv" + } + ], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "data/missing-file.csv", + "@type": "File", + "name": "missing-file.csv", + "description": "A CSV file that is not included in the payload but has localPath.", + "encodingFormat": "text/csv", + "contentSize": "1024", + "localPath": "/mnt/archive/data/missing-file.csv" + }, + { + "@id": "#output-file.csv", + "@type": "File", + "name": "output-file.csv", + "description": "A deliberately absent file with localPath.", + "encodingFormat": "text/csv", + "contentSize": "2048", + "localPath": "/tmp/output-file.csv" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/invalid/data.csv b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/invalid/data.csv new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..236c066f6 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/invalid/ro-crate-metadata.json @@ -0,0 +1,63 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "File with invalid conformsTo — invalid", + "description": "This RO-Crate has a File Data Entity with conformsTo referencing a Person (not a Profile or CreativeWork).", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "publisher": {"@id": "#publisher-org"}, + "funder": {"@id": "https://ror.org/00k4n6c32"}, + "hasPart": [{"@id": "data.csv"}], + "identifier": {"@id": "#uuid"} + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data file", + "description": "A CSV file with an invalid conformsTo reference.", + "encodingFormat": "text/csv", + "contentSize": "2048", + "conformsTo": {"@id": "https://orcid.org/0000-0001-2345-6789"} + }, + { + "@id": "https://orcid.org/0000-0001-2345-6789", + "@type": "Person", + "name": "Dr. Jane Doe" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/valid/data.csv b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/valid/data.csv new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/valid/ro-crate-metadata.json new file mode 100644 index 000000000..e980051d1 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_conformsto/valid/ro-crate-metadata.json @@ -0,0 +1,98 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "File with conformsTo — valid", + "description": "This RO-Crate has a File Data Entity with conformsTo referencing a CreativeWork profile.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "hasPart": { + "@id": "data.csv" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data file", + "description": "A CSV file conforming to a format profile.", + "encodingFormat": "text/csv", + "contentSize": "2048", + "conformsTo": { + "@id": "#csv-profile" + } + }, + { + "@id": "#csv-profile", + "@type": "CreativeWork", + "name": "CSV Profile for RO-Crate 1.3", + "description": "A profile describing the expected structure of CSV files used in RO-Crates." + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/invalid/data.csv b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/invalid/data.csv new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..2e1e0f477 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/invalid/ro-crate-metadata.json @@ -0,0 +1,56 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "File without contentSize — invalid", + "description": "This RO-Crate has a File Data Entity missing the recommended contentSize property.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "publisher": {"@id": "#publisher-org"}, + "funder": {"@id": "https://ror.org/00k4n6c32"}, + "hasPart": [{"@id": "data.csv"}], + "identifier": {"@id": "#uuid"} + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data file", + "description": "A sample CSV data file without contentSize.", + "encodingFormat": "text/csv" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/valid/data.csv b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/valid/data.csv new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/valid/ro-crate-metadata.json new file mode 100644 index 000000000..04ef97578 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_contentSize/valid/ro-crate-metadata.json @@ -0,0 +1,89 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "File with contentSize — valid", + "description": "This RO-Crate has a File Data Entity with the recommended contentSize property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "hasPart": { + "@id": "data.csv" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data file", + "description": "A sample CSV data file with contentSize.", + "encodingFormat": "text/csv", + "contentSize": "2048" + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/invalid/data.csv b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/invalid/data.csv new file mode 100644 index 000000000..9e1228cfb --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/invalid/data.csv @@ -0,0 +1,2 @@ +id,value +1,a diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..f769f2cfe --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/invalid/ro-crate-metadata.json @@ -0,0 +1,78 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Example: contextual entity linked from other entities", + "description": "This RO-Crate demonstrates the RO-Crate 1.3 requirement that any contextual entity in the @graph SHOULD be linked to from at least one of the other entities using its @id. The Person entity below is correctly referenced from the Root Data Entity via the author property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0000-0002-1825-0097" + }, + "hasPart": [ + { + "@id": "data.csv" + } + ] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://orcid.org/0000-0002-1825-0097", + "@type": "Person", + "name": "Josiah Carberry", + "contactPoint": { + "@id": "#josiah-carberry-contact" + }, + "affiliation": { + "@id": "https://ror.org/05f9q8d28" + } + }, + { + "@id": "#josiah-carberry-contact", + "@type": "ContactPoint", + "name": "Josiah Carberry Contact", + "email": "mailto:josiah.carberry@example.com" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example University", + "description": "An example university that the Person entity is affiliated with.", + "url": "https://www.exampleuniversity.edu" + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data", + "description": "A sample data file.", + "contentSize": "42", + "contentLocation": { + "@id": "#location" + } + }, + { + "@id": "#location", + "@type": "Place", + "name": "Example location" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/valid/data.csv b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/valid/data.csv new file mode 100644 index 000000000..9e1228cfb --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/valid/data.csv @@ -0,0 +1,2 @@ +id,value +1,a diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/valid/ro-crate-metadata.json new file mode 100644 index 000000000..6ad94da35 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_encoding_format/valid/ro-crate-metadata.json @@ -0,0 +1,102 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Example: contextual entity linked from other entities", + "description": "This RO-Crate demonstrates the RO-Crate 1.3 requirement that any contextual entity in the @graph SHOULD be linked to from at least one of the other entities using its @id. The Person entity below is correctly referenced from the Root Data Entity via the author property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0000-0002-1825-0097" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": { + "@id": "data.csv" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://orcid.org/0000-0002-1825-0097", + "@type": "Person", + "name": "Josiah Carberry", + "contactPoint": { + "@id": "#josiah-carberry-contact" + }, + "affiliation": { + "@id": "https://ror.org/05f9q8d28" + } + }, + { + "@id": "#josiah-carberry-contact", + "@type": "ContactPoint", + "name": "Josiah Carberry Contact", + "email": "mailto:josiah.carberry@example.com" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example University", + "description": "An example university that the Person entity is affiliated with.", + "url": "https://www.exampleuniversity.edu", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data", + "description": "A sample data file.", + "encodingFormat": "text/csv", + "contentSize": "42", + "contentLocation": { + "@id": "#location" + } + }, + { + "@id": "#location", + "@type": "Place", + "name": "Example location" + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example University", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/invalid/my-test-data.csv b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/invalid/my-test-data.csv new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..a3b86b2b1 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/invalid/ro-crate-metadata.json @@ -0,0 +1,41 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Counterexample: preview incorrectly in hasPart", + "description": "This RO-Crate INCORRECTLY includes ro-crate-preview.html in hasPart. In RO-Crate 1.3, ro-crate-preview.html SHOULD NOT be listed in hasPart of the Root Data Entity or any Dataset entity (unless it is also the profile description in a Profile Crate).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "hasPart": [ + { + "@id": "my-test-data.csv" + } + ] + }, + { + "@id": "my-test-data.csv", + "@type": "File", + "encodingFormat": "text/csv" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/valid/data.csv b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/valid/data.csv new file mode 100644 index 000000000..9e1228cfb --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/valid/data.csv @@ -0,0 +1,2 @@ +id,value +1,a diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/valid/ro-crate-metadata.json new file mode 100644 index 000000000..6ad94da35 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_properties/valid/ro-crate-metadata.json @@ -0,0 +1,102 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Example: contextual entity linked from other entities", + "description": "This RO-Crate demonstrates the RO-Crate 1.3 requirement that any contextual entity in the @graph SHOULD be linked to from at least one of the other entities using its @id. The Person entity below is correctly referenced from the Root Data Entity via the author property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "author": { + "@id": "https://orcid.org/0000-0002-1825-0097" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": { + "@id": "data.csv" + }, + "funder": { + "@id": "https://ror.org/05f9q8d28" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://orcid.org/0000-0002-1825-0097", + "@type": "Person", + "name": "Josiah Carberry", + "contactPoint": { + "@id": "#josiah-carberry-contact" + }, + "affiliation": { + "@id": "https://ror.org/05f9q8d28" + } + }, + { + "@id": "#josiah-carberry-contact", + "@type": "ContactPoint", + "name": "Josiah Carberry Contact", + "email": "mailto:josiah.carberry@example.com" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example University", + "description": "An example university that the Person entity is affiliated with.", + "url": "https://www.exampleuniversity.edu", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "data.csv", + "@type": "File", + "name": "Sample data", + "description": "A sample data file.", + "encodingFormat": "text/csv", + "contentSize": "42", + "contentLocation": { + "@id": "#location" + } + }, + { + "@id": "#location", + "@type": "Place", + "name": "Example location" + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example University", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_sdDatePublished/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_sdDatePublished/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..f2cd6528e --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_sdDatePublished/invalid/ro-crate-metadata.json @@ -0,0 +1,57 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Web-based File without sdDatePublished — invalid", + "description": "This RO-Crate has a web-based File Data Entity missing the recommended sdDatePublished property.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "publisher": {"@id": "#publisher-org"}, + "funder": {"@id": "https://ror.org/00k4n6c32"}, + "hasPart": [{"@id": "https://example.com/data.csv"}], + "identifier": {"@id": "#uuid"} + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://example.com/data.csv", + "@type": "File", + "name": "Remote data file", + "description": "A web-based CSV file without sdDatePublished.", + "encodingFormat": "text/csv", + "contentSize": "512" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_sdDatePublished/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_sdDatePublished/valid/ro-crate-metadata.json new file mode 100644 index 000000000..caa5d7a1c --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/recommended_sdDatePublished/valid/ro-crate-metadata.json @@ -0,0 +1,90 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Web-based File with sdDatePublished — valid", + "description": "This RO-Crate has a web-based File Data Entity with the recommended sdDatePublished property.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "hasPart": { + "@id": "https://example.com/data.csv" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://example.com/data.csv", + "@type": "File", + "name": "Remote data file", + "description": "A web-based CSV file with sdDatePublished.", + "encodingFormat": "text/csv", + "contentSize": "512", + "sdDatePublished": "2024-06-15" + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_dataset_distribution/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_dataset_distribution/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..ab7bc3de4 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_dataset_distribution/invalid/ro-crate-metadata.json @@ -0,0 +1,34 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Dataset distribution — invalid", + "description": "RO-Crate whose Root Data Entity declares a distribution pointing to a DataDownload that is not downloadable.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "hasPart": [], + "distribution": {"@id": "https://example.com/unavailable-archive.zip"} + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://example.com/unavailable-archive.zip", + "@type": "DataDownload", + "name": "Unavailable archive", + "description": "A DataDownload whose URL is not downloadable (returns HTML or is unreachable).", + "encodingFormat": "application/zip" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_dataset_distribution/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_dataset_distribution/valid/ro-crate-metadata.json new file mode 100644 index 000000000..a4735803a --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_dataset_distribution/valid/ro-crate-metadata.json @@ -0,0 +1,89 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Dataset distribution — valid", + "description": "RO-Crate whose Root Data Entity declares a distribution pointing to a downloadable DataDownload archive.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": [], + "distribution": { + "@id": "https://example.com/rocrate.zip" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://example.com/rocrate.zip", + "@type": "DataDownload", + "name": "RO-Crate archive", + "description": "A ZIP archive of the RO-Crate, directly downloadable.", + "encodingFormat": "application/zip" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_directory_distribution/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_directory_distribution/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..1ddf0f579 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_directory_distribution/invalid/ro-crate-metadata.json @@ -0,0 +1,32 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Web directory without distribution — invalid", + "description": "RO-Crate containing a web-based Directory Data Entity that does not declare a distribution.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "hasPart": [{"@id": "https://example.com/dataset-dir/"}] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://example.com/dataset-dir/", + "@type": "Dataset", + "name": "Web dataset directory (no distribution)", + "description": "A web-based directory that does not declare a distribution property." + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_directory_distribution/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_directory_distribution/valid/ro-crate-metadata.json new file mode 100644 index 000000000..55f65122d --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_directory_distribution/valid/ro-crate-metadata.json @@ -0,0 +1,97 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Web directory with distribution — valid", + "description": "RO-Crate containing a web-based Directory Data Entity that declares a distribution.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": { + "@id": "https://example.com/dataset-dir/" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://example.com/dataset-dir/", + "@type": "Dataset", + "name": "Web dataset directory", + "description": "A web-based directory with a distribution pointing to a downloadable archive.", + "distribution": { + "@id": "https://example.com/dataset-dir.zip" + } + }, + { + "@id": "https://example.com/dataset-dir.zip", + "@type": "DataDownload", + "name": "Dataset archive", + "description": "ZIP archive of the web dataset directory.", + "encodingFormat": "application/zip" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_content_url/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_content_url/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..3c00fa96c --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_content_url/invalid/ro-crate-metadata.json @@ -0,0 +1,50 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Web entity contentUrl — invalid", + "description": "RO-Crate with a web-based File Data Entity whose contentUrl is not downloadable (returns HTML).", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "hasPart": [{"@id": "https://example.com/landing/dataset"}], + "publisher": {"@id": "#publisher-org"}, + "funder": {"@id": "#publisher-org"}, + "identifier": {"@id": "#uuid"} + }, + { + "@id": "#publisher-org", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://example.com/landing/dataset", + "@type": "File", + "name": "Dataset with broken contentUrl", + "description": "A web entity whose contentUrl resolves to an HTML page rather than a direct download.", + "encodingFormat": "application/zip", + "contentUrl": "https://cdn.example.com/broken-link" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_content_url/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_content_url/valid/ro-crate-metadata.json new file mode 100644 index 000000000..2542b4c95 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_content_url/valid/ro-crate-metadata.json @@ -0,0 +1,91 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Web entity contentUrl — valid", + "description": "RO-Crate with a web-based File Data Entity that declares a downloadable contentUrl.", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": { + "@id": "https://example.com/landing/dataset" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://example.com/landing/dataset", + "@type": "File", + "name": "Dataset with contentUrl", + "description": "A web entity whose primary @id is a landing page, but contentUrl provides a direct download link.", + "encodingFormat": "application/zip", + "contentUrl": "https://cdn.example.com/dataset.zip", + "contentSize": "1024", + "sdDatePublished": "2024-01-01" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_downloadable/invalid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_downloadable/invalid/ro-crate-metadata.json new file mode 100644 index 000000000..655aad2dc --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_downloadable/invalid/ro-crate-metadata.json @@ -0,0 +1,33 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Web entity downloadable — invalid", + "description": "RO-Crate with a web-based File Data Entity whose @id returns text/html (splash page), making it not directly downloadable.", + "datePublished": "2024-01-01", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "hasPart": [{"@id": "https://example.com/splash-page"}] + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://example.com/splash-page", + "@type": "File", + "name": "Splash page (not downloadable)", + "description": "A web-based entity that resolves to an HTML splash page instead of a downloadable resource.", + "encodingFormat": "text/html" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_downloadable/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_downloadable/valid/ro-crate-metadata.json new file mode 100644 index 000000000..0ff8f4d4d --- /dev/null +++ b/tests/data/crates/rocrate-1.3/8_metadata_dataEntities/web_entity_downloadable/valid/ro-crate-metadata.json @@ -0,0 +1,90 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Web entity downloadable — valid", + "description": "RO-Crate with a web-based File Data Entity whose @id returns a non-HTML Content-Type (directly downloadable).", + "datePublished": "2024-01-01", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "publisher": { + "@id": "https://ror.org/05f9q8d28" + }, + "hasPart": { + "@id": "https://example.com/data.csv" + }, + "funder": { + "@id": "https://ror.org/00k4n6c32" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://ror.org/05f9q8d28", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "CC BY 4.0 license." + }, + { + "@id": "https://example.com/data.csv", + "@type": "File", + "name": "Sample data file", + "description": "A web-based CSV file that is directly downloadable.", + "encodingFormat": "text/csv", + "contentSize": "512", + "sdDatePublished": "2024-01-01" + }, + { + "@id": "https://ror.org/00k4n6c32", + "@type": "Organization", + "name": "European Commission", + "description": "The European Commission funds research and innovation programmes.", + "url": "https://ec.europa.eu", + "contactPoint": { + "@id": "#contact-point-1" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + }, + { + "@id": "#contact-point-1", + "@type": "ContactPoint", + "name": "Contact for European Commission", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_md_about/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_md_about/ro-crate-metadata.json new file mode 100644 index 000000000..3b060e95b --- /dev/null +++ b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_md_about/ro-crate-metadata.json @@ -0,0 +1,62 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "about": {"@id": "./"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Crate with referenced RO-Crate metadata descriptor with about", + "description": "The metadata descriptor of the referenced RO-Crate has about which should not be present.", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "datePublished": "2024-01-01", + "publisher": {"@id": "https://ror.org/012345678"}, + "hasPart": [ + {"@id": "https://example.org/other-ro-crate/"} + ] + }, + { + "@id": "https://example.org/other-ro-crate/", + "@type": "Dataset", + "name": "Other RO-Crate", + "description": "A referenced RO-Crate whose metadata descriptor has about.", + "conformsTo": [ + {"@id": "https://w3id.org/ro/crate"}, + {"@id": "https://w3id.org/ro/crate/1.3"} + ], + "subjectOf": {"@id": "#other-ro-crate-metadata"} + }, + { + "@id": "#other-ro-crate-metadata", + "@type": "CreativeWork", + "encodingFormat": "application/ld+json", + "about": {"@id": "https://example.org/other-ro-crate/"} + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "https://w3id.org/ro/crate", + "@type": "Profile", + "name": "RO-Crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3", + "@type": "Profile", + "name": "RO-Crate 1.3" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_md_conformsto/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_md_conformsto/ro-crate-metadata.json new file mode 100644 index 000000000..a7f4ec4b2 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_md_conformsto/ro-crate-metadata.json @@ -0,0 +1,62 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "about": {"@id": "./"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Crate with referenced RO-Crate metadata descriptor with conformsTo", + "description": "The metadata descriptor of the referenced RO-Crate has conformsTo which should not be present.", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "datePublished": "2024-01-01", + "publisher": {"@id": "https://ror.org/012345678"}, + "hasPart": [ + {"@id": "https://example.org/other-ro-crate/"} + ] + }, + { + "@id": "https://example.org/other-ro-crate/", + "@type": "Dataset", + "name": "Other RO-Crate", + "description": "A referenced RO-Crate whose metadata descriptor has conformsTo.", + "conformsTo": [ + {"@id": "https://w3id.org/ro/crate"}, + {"@id": "https://w3id.org/ro/crate/1.3"} + ], + "subjectOf": {"@id": "#other-ro-crate-metadata"} + }, + { + "@id": "#other-ro-crate-metadata", + "@type": "CreativeWork", + "encodingFormat": "application/ld+json", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"} + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "https://w3id.org/ro/crate", + "@type": "Profile", + "name": "RO-Crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3", + "@type": "Profile", + "name": "RO-Crate 1.3" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_md_encoding_format/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_md_encoding_format/ro-crate-metadata.json new file mode 100644 index 000000000..c19e6a7d9 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_md_encoding_format/ro-crate-metadata.json @@ -0,0 +1,61 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "about": {"@id": "./"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Crate with referenced RO-Crate metadata descriptor with wrong encodingFormat", + "description": "The metadata descriptor of the referenced RO-Crate has the wrong encodingFormat.", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "datePublished": "2024-01-01", + "publisher": {"@id": "https://ror.org/012345678"}, + "hasPart": [ + {"@id": "https://example.org/other-ro-crate/"} + ] + }, + { + "@id": "https://example.org/other-ro-crate/", + "@type": "Dataset", + "name": "Other RO-Crate", + "description": "A referenced RO-Crate with wrong encodingFormat on its metadata descriptor.", + "conformsTo": [ + {"@id": "https://w3id.org/ro/crate"}, + {"@id": "https://w3id.org/ro/crate/1.3"} + ], + "subjectOf": {"@id": "#other-ro-crate-metadata"} + }, + { + "@id": "#other-ro-crate-metadata", + "@type": "CreativeWork", + "encodingFormat": "text/turtle" + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "https://w3id.org/ro/crate", + "@type": "Profile", + "name": "RO-Crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3", + "@type": "Profile", + "name": "RO-Crate 1.3" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_missing_sddatepublished/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_missing_sddatepublished/ro-crate-metadata.json new file mode 100644 index 000000000..93a8eb998 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_missing_sddatepublished/ro-crate-metadata.json @@ -0,0 +1,113 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + }, + "name": "RO-Crate Metadata Descriptor" + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Crate referencing another RO-Crate (missing sdDatePublished)", + "description": "An RO-Crate referencing another RO-Crate whose @id is an absolute URI not declared as a persistent identifier; the referenced entity lacks sdDatePublished.", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "datePublished": "2024-01-01", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "publisher": { + "@id": "https://ror.org/012345678" + }, + "funder": { + "@id": "https://ror.org/012345678" + }, + "hasPart": [ + { + "@id": "https://example.org/other-ro-crate/" + } + ], + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://example.org/other-ro-crate/", + "@type": "Dataset", + "name": "Other RO-Crate", + "description": "A referenced RO-Crate with neither identifier nor sdDatePublished.", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3" + } + ], + "subjectOf": { + "@id": "#other-ro-crate-metadata" + }, + "distribution": { + "@id": "https://example.org/other-ro-crate/archive.tar.gz" + } + }, + { + "@id": "#other-ro-crate-metadata", + "@type": "CreativeWork", + "name": "Other RO-Crate Metadata Descriptor", + "encodingFormat": "application/ld+json" + }, + { + "@id": "https://example.org/other-ro-crate/archive.tar.gz", + "@type": "DataDownload", + "name": "Other RO-Crate Archive", + "encodingFormat": "application/gzip" + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "https://w3id.org/ro/crate", + "@type": ["CreativeWork", "Profile"], + "name": "RO-Crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3", + "@type": ["CreativeWork", "Profile"], + "name": "RO-Crate 1.3" + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_no_subjectof/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_no_subjectof/ro-crate-metadata.json new file mode 100644 index 000000000..8ed56c37d --- /dev/null +++ b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_no_subjectof/ro-crate-metadata.json @@ -0,0 +1,55 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "about": {"@id": "./"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Crate referencing another RO-Crate without subjectOf", + "description": "An RO-Crate that references another RO-Crate missing the subjectOf property.", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "datePublished": "2024-01-01", + "publisher": {"@id": "https://ror.org/012345678"}, + "hasPart": [ + {"@id": "https://example.org/other-ro-crate/"} + ] + }, + { + "@id": "https://example.org/other-ro-crate/", + "@type": "Dataset", + "name": "Other RO-Crate", + "description": "A referenced RO-Crate without subjectOf.", + "conformsTo": [ + {"@id": "https://w3id.org/ro/crate"}, + {"@id": "https://w3id.org/ro/crate/1.3"} + ] + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "https://w3id.org/ro/crate", + "@type": "Profile", + "name": "RO-Crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3", + "@type": "Profile", + "name": "RO-Crate 1.3" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_no_versionless_conformsto/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_no_versionless_conformsto/ro-crate-metadata.json new file mode 100644 index 000000000..5b27f6654 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_no_versionless_conformsto/ro-crate-metadata.json @@ -0,0 +1,53 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "about": {"@id": "./"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Crate referencing another RO-Crate without version-less conformsTo", + "description": "An RO-Crate that references another RO-Crate missing the version-less profile URI.", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "datePublished": "2024-01-01", + "publisher": {"@id": "https://ror.org/012345678"}, + "hasPart": [ + {"@id": "https://example.org/other-ro-crate/"} + ] + }, + { + "@id": "https://example.org/other-ro-crate/", + "@type": "Dataset", + "name": "Other RO-Crate", + "description": "A referenced RO-Crate missing the version-less profile URI.", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "subjectOf": {"@id": "#other-ro-crate-metadata"} + }, + { + "@id": "#other-ro-crate-metadata", + "@type": "CreativeWork", + "encodingFormat": "application/ld+json" + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "https://w3id.org/ro/crate/1.3", + "@type": "Profile", + "name": "RO-Crate 1.3" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_root_conformsto_versionless/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_root_conformsto_versionless/ro-crate-metadata.json new file mode 100644 index 000000000..334633225 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/invalid_root_conformsto_versionless/ro-crate-metadata.json @@ -0,0 +1,66 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "about": {"@id": "./"} + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Crate with version-less conformsTo on Root", + "description": "An RO-Crate whose Root Data Entity incorrectly declares the version-less generic profile URI.", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + "datePublished": "2024-01-01", + "publisher": {"@id": "https://ror.org/012345678"}, + "conformsTo": [ + {"@id": "https://w3id.org/ro/crate"}, + {"@id": "https://w3id.org/ro/crate/1.3"} + ], + "hasPart": [ + {"@id": "https://example.org/other-ro-crate/"} + ] + }, + { + "@id": "https://example.org/other-ro-crate/", + "@type": "Dataset", + "name": "Other RO-Crate", + "description": "A referenced RO-Crate.", + "conformsTo": [ + {"@id": "https://w3id.org/ro/crate"}, + {"@id": "https://w3id.org/ro/crate/1.3"} + ], + "subjectOf": {"@id": "#other-ro-crate-metadata"} + }, + { + "@id": "#other-ro-crate-metadata", + "@type": "CreativeWork", + "name": "Other RO-Crate Metadata Descriptor", + "encodingFormat": "application/ld+json" + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "https://w3id.org/ro/crate", + "@type": "Profile", + "name": "RO-Crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3", + "@type": "Profile", + "name": "RO-Crate 1.3" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid/ro-crate-metadata.json new file mode 100644 index 000000000..0b3cb6a54 --- /dev/null +++ b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid/ro-crate-metadata.json @@ -0,0 +1,112 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + }, + "name": "RO-Crate Metadata Descriptor" + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Crate referencing another RO-Crate", + "description": "An RO-Crate that references another RO-Crate as a data entity.", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "datePublished": "2024-01-01", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "publisher": { + "@id": "https://ror.org/012345678" + }, + "funder": { + "@id": "https://ror.org/012345678" + }, + "hasPart": { + "@id": "https://example.org/other-ro-crate/" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://example.org/other-ro-crate/", + "@type": "Dataset", + "name": "Other RO-Crate", + "description": "A referenced RO-Crate.", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3" + } + ], + "subjectOf": { + "@id": "#other-ro-crate-metadata" + }, + "sdDatePublished": "2024-01-01", + "distribution": { + "@id": "https://example.org/other-ro-crate/archive.tar.gz" + } + }, + { + "@id": "#other-ro-crate-metadata", + "@type": "CreativeWork", + "name": "Other RO-Crate Metadata Descriptor", + "encodingFormat": "application/ld+json" + }, + { + "@id": "https://example.org/other-ro-crate/archive.tar.gz", + "@type": "DataDownload", + "name": "Other RO-Crate Archive", + "encodingFormat": "application/gzip" + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "https://w3id.org/ro/crate", + "@type": ["CreativeWork", "Profile"], + "name": "RO-Crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3", + "@type": ["CreativeWork", "Profile"], + "name": "RO-Crate 1.3" + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid_with_identifier/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid_with_identifier/ro-crate-metadata.json new file mode 100644 index 000000000..c62ba3c7f --- /dev/null +++ b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid_with_identifier/ro-crate-metadata.json @@ -0,0 +1,112 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + }, + "name": "RO-Crate Metadata Descriptor" + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Crate referencing another RO-Crate with declared identifier", + "description": "An RO-Crate referencing another RO-Crate whose @id is a declared persistent identifier (case #1): no sdDatePublished required.", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "datePublished": "2024-01-01", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "publisher": { + "@id": "https://ror.org/012345678" + }, + "funder": { + "@id": "https://ror.org/012345678" + }, + "hasPart": { + "@id": "https://example.org/persistent-crate/" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "https://example.org/persistent-crate/", + "@type": "Dataset", + "name": "Other RO-Crate with persistent identifier", + "description": "A referenced RO-Crate whose @id IS a declared persistent identifier (DOI).", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3" + } + ], + "identifier": "10.5281/zenodo.1234567", + "subjectOf": { + "@id": "#other-ro-crate-metadata" + }, + "distribution": { + "@id": "https://example.org/persistent-crate//archive.zip" + } + }, + { + "@id": "https://example.org/persistent-crate//archive.zip", + "@type": "DataDownload", + "name": "Other RO-Crate Archive", + "encodingFormat": "application/zip" + }, + { + "@id": "#other-ro-crate-metadata", + "@type": "CreativeWork", + "name": "Other RO-Crate Metadata Descriptor", + "encodingFormat": "application/ld+json" + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "https://w3id.org/ro/crate", + "@type": ["CreativeWork", "Profile"], + "name": "RO-Crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3", + "@type": ["CreativeWork", "Profile"], + "name": "RO-Crate 1.3" + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid_with_relative_path/ro-crate-metadata.json b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid_with_relative_path/ro-crate-metadata.json new file mode 100644 index 000000000..5a78a58fe --- /dev/null +++ b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid_with_relative_path/ro-crate-metadata.json @@ -0,0 +1,113 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + }, + "name": "RO-Crate Metadata Descriptor" + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Attached crate referencing a nested RO-Crate by relative path", + "description": "An attached RO-Crate (A) referencing another nested RO-Crate (B) via a relative path (case #2): no sdDatePublished required.", + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "datePublished": "2024-01-01", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "publisher": { + "@id": "https://ror.org/012345678" + }, + "funder": { + "@id": "https://ror.org/012345678" + }, + "hasPart": { + "@id": "subcrate/" + }, + "identifier": { + "@id": "#uuid" + } + }, + { + "@id": "#uuid", + "@type": "PropertyValue", + "name": "UUID", + "propertyID": "UUID", + "value": "550e8400-e29b-41d4-a716-446655440000" + }, + { + "@id": "subcrate/", + "@type": "Dataset", + "name": "Nested RO-Crate", + "description": "A referenced RO-Crate nested within A, identified by relative path.", + "conformsTo": [ + { + "@id": "https://w3id.org/ro/crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3" + } + ], + "hasPart": { + "@id": "subcrate/data.txt" + }, + "subjectOf": { + "@id": "#subcrate-metadata" + } + }, + { + "@id": "subcrate/data.txt", + "@type": "File", + "name": "Nested data file", + "description": "A data file inside the nested RO-Crate.", + "encodingFormat": "text/plain", + "contentSize": "0" + }, + { + "@id": "#subcrate-metadata", + "@type": "CreativeWork", + "name": "Nested RO-Crate Metadata Descriptor", + "encodingFormat": "application/ld+json" + }, + { + "@id": "https://ror.org/012345678", + "@type": "Organization", + "name": "Example Research Institute", + "url": "https://example.org", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license." + }, + { + "@id": "https://w3id.org/ro/crate", + "@type": ["CreativeWork", "Profile"], + "name": "RO-Crate" + }, + { + "@id": "https://w3id.org/ro/crate/1.3", + "@type": ["CreativeWork", "Profile"], + "name": "RO-Crate 1.3" + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact for Example Research Institute", + "email": "mailto:contact@example.org" + } + ] +} diff --git a/tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid_with_relative_path/subcrate/data.txt b/tests/data/crates/rocrate-1.3/9_referenced_rocrate/valid_with_relative_path/subcrate/data.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/crates/valid/detached/dataset-ro-crate-metadata.json b/tests/data/crates/valid/detached-1.2/dataset-ro-crate-metadata.json similarity index 100% rename from tests/data/crates/valid/detached/dataset-ro-crate-metadata.json rename to tests/data/crates/valid/detached-1.2/dataset-ro-crate-metadata.json diff --git a/tests/data/crates/valid/detached/test-ro-crate-metadata.json b/tests/data/crates/valid/detached-1.2/test-ro-crate-metadata.json similarity index 100% rename from tests/data/crates/valid/detached/test-ro-crate-metadata.json rename to tests/data/crates/valid/detached-1.2/test-ro-crate-metadata.json diff --git a/tests/data/crates/valid/detached-1.3/dataset-ro-crate-metadata.json b/tests/data/crates/valid/detached-1.3/dataset-ro-crate-metadata.json new file mode 100644 index 000000000..05f8fa26a --- /dev/null +++ b/tests/data/crates/valid/detached-1.3/dataset-ro-crate-metadata.json @@ -0,0 +1,43 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://example.org/ro-crate/detached" + } + }, + { + "@id": "https://example.org/ro-crate/detached", + "@type": "Dataset", + "name": "Detached RO-Crate 1.3", + "description": "Detached RO-Crate metadata-only file.", + "license": { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/" + }, + "datePublished": "2024-01-01", + "hasPart": [ + { + "@id": "https://example.org/data/file1.txt" + } + ] + }, + { + "@id": "https://example.org/data/file1.txt", + "@type": "File", + "name": "file1.txt", + "description": "Remote file in detached RO-Crate.", + "encodingFormat": "text/plain" + }, + { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/", + "@type": "CreativeWork", + "name": "Creative Commons Zero 1.0 Universal", + "description": "A Creative Commons license that allows others to copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission. This is the most permissive of the Creative Commons licenses, effectively placing the work in the public domain." + } + ] +} diff --git a/tests/data/crates/valid/detached-1.3/test-ro-crate-metadata.json b/tests/data/crates/valid/detached-1.3/test-ro-crate-metadata.json new file mode 100644 index 000000000..dcfa146e9 --- /dev/null +++ b/tests/data/crates/valid/detached-1.3/test-ro-crate-metadata.json @@ -0,0 +1,37 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "test-ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://example.org/ro-crate/detached/prefixed" + } + }, + { + "@id": "https://example.org/ro-crate/detached/prefixed", + "@type": "Dataset", + "name": "Detached RO-Crate (prefixed metadata)", + "description": "Detached RO-Crate with prefixed metadata filename.", + "license": { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/" + }, + "datePublished": "2024-01-01", + "hasPart": [ + { + "@id": "https://example.org/data/file1.txt" + } + ] + }, + { + "@id": "https://example.org/data/file1.txt", + "@type": "File", + "name": "file1.txt", + "description": "Remote file in detached RO-Crate.", + "encodingFormat": "text/plain" + } + ] +} diff --git a/tests/data/crates/valid/ro-crate-1.2-absolute-root/prefix-ro-crate-metadata.json b/tests/data/crates/valid/ro-crate-1.2-absolute-root/ro-crate-metadata.json similarity index 96% rename from tests/data/crates/valid/ro-crate-1.2-absolute-root/prefix-ro-crate-metadata.json rename to tests/data/crates/valid/ro-crate-1.2-absolute-root/ro-crate-metadata.json index 71cb8c004..8875e0a58 100644 --- a/tests/data/crates/valid/ro-crate-1.2-absolute-root/prefix-ro-crate-metadata.json +++ b/tests/data/crates/valid/ro-crate-1.2-absolute-root/ro-crate-metadata.json @@ -26,11 +26,9 @@ "@id": "https://ror.org/123456789" }, "datePublished": "2024-01-01", - "hasPart": [ - { - "@id": "https://raw.githubusercontent.com/crs4/pydoop/refs/heads/develop/README.md" - } - ], + "hasPart": { + "@id": "https://raw.githubusercontent.com/crs4/pydoop/refs/heads/develop/README.md" + }, "cite-as": { "@id": "https://github.com/crs4/pydoop/tree/develop/" }, diff --git a/tests/data/crates/valid/ro-crate-1.3-absolute-root/ro-crate-metadata.json b/tests/data/crates/valid/ro-crate-1.3-absolute-root/ro-crate-metadata.json new file mode 100644 index 000000000..14e769c19 --- /dev/null +++ b/tests/data/crates/valid/ro-crate-1.3-absolute-root/ro-crate-metadata.json @@ -0,0 +1,125 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "https://github.com/crs4/pydoop/tree/develop/" + } + }, + { + "@id": "https://github.com/crs4/pydoop/tree/develop/", + "@type": "Dataset", + "name": "Example RO-Crate 1.3 (absolute root)", + "description": "Attached RO-Crate using an absolute root @id.", + "license": { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/" + }, + "publisher": { + "@id": "https://orcid.org/0000-0001-2345-6789" + }, + "funder": { + "@id": "https://ror.org/123456789" + }, + "datePublished": "2024-01-01", + "hasPart": { + "@id": "https://raw.githubusercontent.com/crs4/pydoop/refs/heads/develop/README.md" + }, + "cite-as": { + "@id": "https://github.com/crs4/pydoop/tree/develop/" + }, + "url": "https://github.com/crs4/pydoop/tree/develop/", + "identifier": { + "@id": "#pydoop-pid" + } + }, + { + "@id": "#pydoop-pid", + "@type": "PropertyValue", + "name": "Pydoop repository URL", + "propertyID": "URL", + "value": "https://github.com/crs4/pydoop/tree/develop/" + }, + { + "@id": "https://raw.githubusercontent.com/crs4/pydoop/refs/heads/develop/README.md", + "@type": "File", + "name": "README.md", + "description": "Example data file.", + "encodingFormat": "text/plain", + "contentSize": "221", + "contentLocation": { + "@id": "#bologna" + }, + "conformsTo": { + "@id": "#csv-profile" + }, + "dateCreated": "2025-06-15", + "dateModified": "2025-12-01", + "author": { + "@id": "https://orcid.org/0000-0001-2345-6789" + }, + "license": { + "@id": "https://creativecommons.org/licenses/by/4.0/" + }, + "sdDatePublished": "2026-04-15" + }, + { + "@id": "#csv-profile", + "@type": "CreativeWork", + "name": "Example CSV Profile", + "description": "An example profile describing CSV files." + }, + { + "@id": "#bologna", + "@type": "Place", + "name": "Bologna, Italy", + "description": "The city of Bologna in Italy." + }, + { + "@id": "https://orcid.org/0000-0001-2345-6789", + "@type": "Person", + "name": "Jane Doe", + "description": "An example author with an ORCID identifier.", + "affiliation": { + "@id": "https://ror.org/123456789" + }, + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "https://ror.org/123456789", + "@type": "Organization", + "name": "Example University", + "description": "An example university affiliated with the author.", + "url": "https://exampleuniversity.edu", + "contactPoint": { + "@id": "#contact-point" + } + }, + { + "@id": "#contact-point", + "@type": "ContactPoint", + "name": "Contact", + "contactType": "customer support", + "email": "info@exampleuniversity.edu", + "url": "https://exampleuniversity.edu/contact" + }, + { + "@id": "https://creativecommons.org/licenses/by/4.0/", + "@type": "CreativeWork", + "name": "Creative Commons Attribution 4.0 International", + "description": "A Creative Commons license that lets others distribute, remix, adapt, and build upon your work, even commercially." + }, + { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/", + "@type": "CreativeWork", + "name": "Creative Commons CC0 1.0 Universal (CC0 1.0) Public Domain Dedication", + "description": "A Creative Commons license that allows creators to waive all their copyright and related rights in their works, effectively placing them in the public domain." + } + ] +} diff --git a/tests/data/crates/valid/ro-crate-1.3-attached/data.txt b/tests/data/crates/valid/ro-crate-1.3-attached/data.txt new file mode 100644 index 000000000..ce0136250 --- /dev/null +++ b/tests/data/crates/valid/ro-crate-1.3-attached/data.txt @@ -0,0 +1 @@ +hello diff --git a/tests/data/crates/valid/ro-crate-1.3-attached/ro-crate-metadata.json b/tests/data/crates/valid/ro-crate-1.3-attached/ro-crate-metadata.json new file mode 100644 index 000000000..96cd2732c --- /dev/null +++ b/tests/data/crates/valid/ro-crate-1.3-attached/ro-crate-metadata.json @@ -0,0 +1,38 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + }, + "about": { + "@id": "./" + } + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Example RO-Crate 1.3", + "description": "Minimal attached RO-Crate for profile 1.3 tests.", + "license": { + "@id": "https://creativecommons.org/publicdomain/zero/1.0/" + }, + "datePublished": "2024-01-01", + "hasPart": [ + { + "@id": "data.txt" + } + ] + }, + { + "@id": "data.txt", + "@type": "File", + "name": "data.txt", + "description": "Example data file.", + "encodingFormat": "text/plain", + "contentSize": "5" + } + ] +} diff --git a/tests/data/crates/valid/workflow-roc-1.1/README.md b/tests/data/crates/valid/workflow-roc-1.1/README.md new file mode 100644 index 000000000..33fbaf728 --- /dev/null +++ b/tests/data/crates/valid/workflow-roc-1.1/README.md @@ -0,0 +1,3 @@ +# Example of a valid Workflow RO-Crate + +The Galaxy workflow has been copied from the LifeMonitor repository: [sort-and-change-case.ga](https://github.com/crs4/life_monitor/blob/b6206ca348701817756e133e72174826a5c3df4a/interaction_experiments/workflow_examples/galaxy/sort-and-change-case/sort-and-change-case.ga). The abstract CWL version of the workflow, `sort-and-change-case.cwl` has been generated by converting the Galaxy one with [ro-crate-py](https://github.com/ResearchObject/ro-crate-py), which, in turn, uses [galaxy2cwl](https://github.com/workflowhub-eu/galaxy2cwl). diff --git a/tests/data/crates/valid/workflow-roc-1.1/blank.png b/tests/data/crates/valid/workflow-roc-1.1/blank.png new file mode 100644 index 000000000..053a72f9e Binary files /dev/null and b/tests/data/crates/valid/workflow-roc-1.1/blank.png differ diff --git a/tests/data/crates/valid/workflow-roc-1.1/examples/dummy_example.txt b/tests/data/crates/valid/workflow-roc-1.1/examples/dummy_example.txt new file mode 100644 index 000000000..c88ab3c1b --- /dev/null +++ b/tests/data/crates/valid/workflow-roc-1.1/examples/dummy_example.txt @@ -0,0 +1 @@ +Dummy example diff --git a/tests/data/crates/valid/workflow-roc-1.1/ro-crate-metadata.json b/tests/data/crates/valid/workflow-roc-1.1/ro-crate-metadata.json new file mode 100644 index 000000000..9d8c74d1a --- /dev/null +++ b/tests/data/crates/valid/workflow-roc-1.1/ro-crate-metadata.json @@ -0,0 +1,191 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": { + "@id": "https://spdx.org/licenses/Apache-2.0.html" + }, + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "https://spdx.org/licenses/Apache-2.0.html", + "@type": "CreativeWork", + "name": "Apache 2.0 license" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow", + "HowTo" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "input": [ + { + "@id": "#input-param" + } + ], + "output": [ + { + "@id": "#output-param" + } + ], + "step": [ + { + "@id": "#sort" + }, + { + "@id": "#change-case" + } + ], + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "#input-param", + "@type": "FormalParameter", + "additionalType": "File", + "name": "input", + "description": "input text file" + }, + { + "@id": "#output-param", + "@type": "FormalParameter", + "additionalType": "File", + "name": "output", + "description": "output text file" + }, + { + "@id": "#sort", + "@type": "HowToStep", + "position": "0" + }, + { + "@id": "#change-case", + "@type": "HowToStep", + "position": "1" + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/valid/workflow-roc-1.1/sort-and-change-case.cwl b/tests/data/crates/valid/workflow-roc-1.1/sort-and-change-case.cwl new file mode 100644 index 000000000..662ae8628 --- /dev/null +++ b/tests/data/crates/valid/workflow-roc-1.1/sort-and-change-case.cwl @@ -0,0 +1,41 @@ +class: Workflow +cwlVersion: v1.2.0-dev2 +doc: 'Abstract CWL Automatically generated from the Galaxy workflow file: sort-and-change-case' +inputs: + 0_Input Dataset: + format: data + type: File +outputs: {} +steps: + 1_Sort: + in: + input: 0_Input Dataset + out: + - out_file1 + run: + class: Operation + id: sort1 + inputs: + input: + format: Any + type: File + outputs: + out_file1: + doc: input + type: File + 2_Change Case: + in: + input: 1_Sort/out_file1 + out: + - out_file1 + run: + class: Operation + id: ChangeCase + inputs: + input: + format: Any + type: File + outputs: + out_file1: + doc: tabular + type: File diff --git a/tests/data/crates/valid/workflow-roc-1.1/sort-and-change-case.ga b/tests/data/crates/valid/workflow-roc-1.1/sort-and-change-case.ga new file mode 100644 index 000000000..5a1999692 --- /dev/null +++ b/tests/data/crates/valid/workflow-roc-1.1/sort-and-change-case.ga @@ -0,0 +1,118 @@ +{ + "uuid": "e2a8566c-c025-4181-9e90-7ed29d4e4df1", + "tags": [], + "format-version": "0.1", + "name": "sort-and-change-case", + "version": 0, + "steps": { + "0": { + "tool_id": null, + "tool_version": null, + "outputs": [], + "workflow_outputs": [], + "input_connections": {}, + "tool_state": "{}", + "id": 0, + "uuid": "5a36fad2-66c7-4b9e-8759-0fbcae9b8541", + "errors": null, + "name": "Input dataset", + "label": "bed_input", + "inputs": [], + "position": { + "top": 200, + "left": 200 + }, + "annotation": "", + "content_id": null, + "type": "data_input" + }, + "1": { + "tool_id": "sort1", + "tool_version": "1.1.0", + "outputs": [ + { + "type": "input", + "name": "out_file1" + } + ], + "workflow_outputs": [ + { + "output_name": "out_file1", + "uuid": "8237f71a-bc2a-494e-a63c-09c1e65ef7c8", + "label": "sorted_bed" + } + ], + "input_connections": { + "input": { + "output_name": "output", + "id": 0 + } + }, + "tool_state": "{\"__page__\": null, \"style\": \"\\\"alpha\\\"\", \"column\": \"\\\"1\\\"\", \"__rerun_remap_job_id__\": null, \"column_set\": \"[]\", \"input\": \"{\\\"__class__\\\": \\\"RuntimeValue\\\"}\", \"header_lines\": \"\\\"0\\\"\", \"order\": \"\\\"ASC\\\"\"}", + "id": 1, + "uuid": "0b6b3cda-c75f-452b-85b1-8ae4f3302ba4", + "errors": null, + "name": "Sort", + "post_job_actions": {}, + "label": "sort", + "inputs": [ + { + "name": "input", + "description": "runtime parameter for tool Sort" + } + ], + "position": { + "top": 200, + "left": 420 + }, + "annotation": "", + "content_id": "sort1", + "type": "tool" + }, + "2": { + "tool_id": "ChangeCase", + "tool_version": "1.0.0", + "outputs": [ + { + "type": "tabular", + "name": "out_file1" + } + ], + "workflow_outputs": [ + { + "output_name": "out_file1", + "uuid": "c31cd733-dab6-4d50-9fec-b644d162397b", + "label": "uppercase_bed" + } + ], + "input_connections": { + "input": { + "output_name": "out_file1", + "id": 1 + } + }, + "tool_state": "{\"__page__\": null, \"casing\": \"\\\"up\\\"\", \"__rerun_remap_job_id__\": null, \"cols\": \"\\\"c1\\\"\", \"delimiter\": \"\\\"TAB\\\"\", \"input\": \"{\\\"__class__\\\": \\\"RuntimeValue\\\"}\"}", + "id": 2, + "uuid": "9698bcde-0729-48fe-b88d-ccfb6f6153b4", + "errors": null, + "name": "Change Case", + "post_job_actions": {}, + "label": "change_case", + "inputs": [ + { + "name": "input", + "description": "runtime parameter for tool Change Case" + } + ], + "position": { + "top": 200, + "left": 640 + }, + "annotation": "", + "content_id": "ChangeCase", + "type": "tool" + } + }, + "annotation": "", + "a_galaxy_workflow": "true" +} diff --git a/tests/data/crates/valid/workflow-roc-1.1/test/dummy_test.txt b/tests/data/crates/valid/workflow-roc-1.1/test/dummy_test.txt new file mode 100644 index 000000000..b70f706a0 --- /dev/null +++ b/tests/data/crates/valid/workflow-roc-1.1/test/dummy_test.txt @@ -0,0 +1 @@ +Dummy test diff --git a/tests/data/crates/valid/workflow-roc-string-license-1.1/ro-crate-metadata.json b/tests/data/crates/valid/workflow-roc-string-license-1.1/ro-crate-metadata.json new file mode 100644 index 000000000..e914d74ad --- /dev/null +++ b/tests/data/crates/valid/workflow-roc-string-license-1.1/ro-crate-metadata.json @@ -0,0 +1,141 @@ +{ + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "conformsTo": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1" + }, + "name": "sort-and-change-case Workflow RO-Crate", + "description": "sort-and-change-case Workflow RO-Crate", + "datePublished": "2024-04-17T13:39:44+00:00", + "hasPart": [ + { + "@id": "sort-and-change-case.ga" + }, + { + "@id": "sort-and-change-case.cwl" + }, + { + "@id": "blank.png" + }, + { + "@id": "README.md" + }, + { + "@id": "test/" + }, + { + "@id": "examples/" + } + ], + "license": "Apache-2.0", + "mainEntity": { + "@id": "sort-and-change-case.ga" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate/1.1", + "@type": [ + "CreativeWork", + "Profile" + ], + "name": "Workflow RO-Crate", + "version": "1.1" + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.3" + } + }, + { + "@id": "sort-and-change-case.ga", + "@type": [ + "File", + "SoftwareSourceCode", + "ComputationalWorkflow" + ], + "conformsTo": { + "@id": "https://bioschemas.org/profiles/ComputationalWorkflow/1.0-RELEASE" + }, + "description": "sort lines and change text to upper case", + "image": { + "@id": "blank.png" + }, + "license": "https://spdx.org/licenses/MIT.html", + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy" + }, + "subjectOf": { + "@id": "sort-and-change-case.cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#galaxy", + "@type": "ComputerLanguage", + "identifier": { + "@id": "https://galaxyproject.org/" + }, + "name": "Galaxy", + "url": { + "@id": "https://galaxyproject.org/" + }, + "version": "23.2" + }, + { + "@id": "sort-and-change-case.cwl", + "@type": [ + "File", + "SoftwareSourceCode", + "HowTo" + ], + "name": "sort-and-change-case", + "programmingLanguage": { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl" + } + }, + { + "@id": "https://w3id.org/workflowhub/workflow-ro-crate#cwl", + "@type": "ComputerLanguage", + "alternateName": "CWL", + "identifier": { + "@id": "https://w3id.org/cwl/" + }, + "name": "Common Workflow Language", + "url": { + "@id": "https://www.commonwl.org/" + }, + "version": "v1.2.0-dev2" + }, + { + "@id": "blank.png", + "@type": [ + "File", + "ImageObject" + ] + }, + { + "@id": "README.md", + "@type": "File", + "about": { + "@id": "./" + }, + "encodingFormat": "text/markdown" + }, + { + "@id": "test/", + "@type": "Dataset" + }, + { + "@id": "examples/", + "@type": "Dataset" + } + ] +} diff --git a/tests/data/crates/valid/wrroc-paper/ro-crate-metadata.json b/tests/data/crates/valid/wrroc-paper/ro-crate-metadata.json index 3f99ec941..bed26e6bc 100644 --- a/tests/data/crates/valid/wrroc-paper/ro-crate-metadata.json +++ b/tests/data/crates/valid/wrroc-paper/ro-crate-metadata.json @@ -40,11 +40,9 @@ "about": { "@id": "https://researchobject.org/workflow-run-crate/" }, - "author": [ - { - "@id": "https://orcid.org/0000-0001-8271-5429" - } - ], + "author": { + "@id": "https://orcid.org/0000-0001-8271-5429" + }, "description": "RO-Crate for the manuscript that describes Workflow Run Crate, includes mapping to PROV using SKOS/SSSOM", "hasPart": [ { @@ -392,11 +390,9 @@ "@id": "https://mapping-commons.github.io/sssom/spec/#tsv" } ], - "author": [ - { - "@id": "https://orcid.org/0000-0001-8271-5429" - } - ], + "author": { + "@id": "https://orcid.org/0000-0001-8271-5429" + }, "creator": { "@id": "https://orcid.org/0000-0001-9842-9718" }, diff --git a/tests/data/profiles/check_overriding/b/profile.ttl b/tests/data/profiles/check_overriding/b/profile.ttl index e947b732d..917a011d3 100644 --- a/tests/data/profiles/check_overriding/b/profile.ttl +++ b/tests/data/profiles/check_overriding/b/profile.ttl @@ -16,6 +16,7 @@ @prefix prof: . @prefix role: . @prefix rdfs: . +@prefix validator: . @@ -33,6 +34,7 @@ # # This profile is an extension of the RO-Crate Metadata Specification 1.1 profile prof:isProfileOf ; + validator:isRuleOverlayOf ; # # Explicitly state that this profile is a transitive profile of the RO-Crate Metadata Specification 1.1 profile prof:isTransitiveProfileOf ; diff --git a/tests/data/profiles/effective_checks/a/checks.py b/tests/data/profiles/effective_checks/a/checks.py new file mode 100644 index 000000000..f9a7fb23b --- /dev/null +++ b/tests/data/profiles/effective_checks/a/checks.py @@ -0,0 +1,15 @@ +from rocrate_validator.models import ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement + + +@requirement(name="Source checks") +class SourceChecks(PyFunctionCheck): + @check(name="Shared check") + def shared_check(self, _context: ValidationContext) -> bool: + """Check implemented by both the source and overlay profiles.""" + return True + + @check(name="Inherited check") + def inherited_check(self, _context: ValidationContext) -> bool: + """Check implemented only by the source profile.""" + return True diff --git a/tests/data/profiles/effective_checks/a/profile.ttl b/tests/data/profiles/effective_checks/a/profile.ttl new file mode 100644 index 000000000..c21d8fdf2 --- /dev/null +++ b/tests/data/profiles/effective_checks/a/profile.ttl @@ -0,0 +1,7 @@ +@prefix prof: . +@prefix rdfs: . + + + a prof:Profile ; + rdfs:label "Effective checks source profile" ; + prof:hasToken "effective-a" . diff --git a/tests/data/profiles/effective_checks/b/checks.py b/tests/data/profiles/effective_checks/b/checks.py new file mode 100644 index 000000000..70430ed6a --- /dev/null +++ b/tests/data/profiles/effective_checks/b/checks.py @@ -0,0 +1,10 @@ +from rocrate_validator.models import ValidationContext +from rocrate_validator.requirements.python import PyFunctionCheck, check, requirement + + +@requirement(name="Overlay checks") +class OverlayChecks(PyFunctionCheck): + @check(name="Shared check") + def shared_check(self, _context: ValidationContext) -> bool: + """Local implementation replacing the source profile check.""" + return True diff --git a/tests/data/profiles/effective_checks/b/profile.ttl b/tests/data/profiles/effective_checks/b/profile.ttl new file mode 100644 index 000000000..347897da4 --- /dev/null +++ b/tests/data/profiles/effective_checks/b/profile.ttl @@ -0,0 +1,10 @@ +@prefix prof: . +@prefix rdfs: . +@prefix validator: . + + + a prof:Profile ; + rdfs:label "Effective checks overlay profile" ; + prof:isProfileOf ; + validator:isRuleOverlayOf ; + prof:hasToken "effective-b" . diff --git a/tests/data/profiles/effective_checks/c/profile.ttl b/tests/data/profiles/effective_checks/c/profile.ttl new file mode 100644 index 000000000..1ec3f2270 --- /dev/null +++ b/tests/data/profiles/effective_checks/c/profile.ttl @@ -0,0 +1,11 @@ +@prefix prof: . +@prefix rdfs: . + + + a prof:Profile ; + rdfs:label "Effective checks wrapper profile" ; + prof:isProfileOf ; + prof:isTransitiveProfileOf + , + ; + prof:hasToken "effective-c" . diff --git a/tests/integration/profiles/five-safes-crate/test_5src_10_outputs.py b/tests/integration/profiles/five-safes-crate/test_5src_10_outputs.py index 15dcc2449..a476b465c 100644 --- a/tests/integration/profiles/five-safes-crate/test_5src_10_outputs.py +++ b/tests/integration/profiles/five-safes-crate/test_5src_10_outputs.py @@ -50,7 +50,7 @@ def test_completed_createaction_does_not_have_result(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["WorkflowRunAction"], + expected_triggered_requirements=["Completed workflow run SHOULD have a result"], expected_triggered_issues=[ "The `CreateAction` corresponding to the workflow run,", "with CompletedActionStatus, SHOULD have the `result` property.", diff --git a/tests/integration/profiles/five-safes-crate/test_5src_11_workflow_execution.py b/tests/integration/profiles/five-safes-crate/test_5src_11_workflow_execution.py index 9f54354d9..3de776bdf 100644 --- a/tests/integration/profiles/five-safes-crate/test_5src_11_workflow_execution.py +++ b/tests/integration/profiles/five-safes-crate/test_5src_11_workflow_execution.py @@ -45,7 +45,7 @@ def test_5src_workflow_object_with_no_name(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["WorkflowExecution"], + expected_triggered_requirements=["Workflow execution MUST have a descriptive name"], expected_triggered_issues=[ "The `CreateAction` corresponding to the workflow run MUST have a name string of at least 10 characters." ], @@ -76,7 +76,7 @@ def test_5src_workflow_object_with_name_not_string(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["WorkflowExecution"], + expected_triggered_requirements=["Workflow execution MUST have a descriptive name"], expected_triggered_issues=[ "The `CreateAction` corresponding to the workflow run MUST have a name string of at least 10 characters." ], @@ -107,7 +107,7 @@ def test_5src_workflow_object_with_not_long_enough_name(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["WorkflowExecution"], + expected_triggered_requirements=["Workflow execution MUST have a descriptive name"], expected_triggered_issues=[ "The `CreateAction` corresponding to the workflow run MUST have a name string of at least 10 characters." ], @@ -200,7 +200,7 @@ def test_5src_workflow_object_with_no_action_status(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["WorkflowExecution"], + expected_triggered_requirements=["Workflow execution MUST have an allowed action status"], expected_triggered_issues=[ ( "`CreateAction` MUST have an actionStatus " @@ -233,7 +233,7 @@ def test_5src_workflow_object_with_no_properly_valued_action_status(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["WorkflowExecution"], + expected_triggered_requirements=["Workflow execution MUST have an allowed action status"], expected_triggered_issues=[ ( "`CreateAction` MUST have an actionStatus " diff --git a/tests/integration/profiles/five-safes-crate/test_5src_12_check_phase.py b/tests/integration/profiles/five-safes-crate/test_5src_12_check_phase.py index 53171a4c0..93f596ba7 100644 --- a/tests/integration/profiles/five-safes-crate/test_5src_12_check_phase.py +++ b/tests/integration/profiles/five-safes-crate/test_5src_12_check_phase.py @@ -50,7 +50,7 @@ def test_5src_check_value_not_of_type_assess_action(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["CheckValue"], + expected_triggered_requirements=["CheckValue MUST have a name and AssessAction type"], expected_triggered_issues=["CheckValue MUST be a `AssessAction`."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -78,7 +78,7 @@ def test_5src_check_value_name_not_a_string(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["CheckValue"], + expected_triggered_requirements=["CheckValue MUST have a name and AssessAction type"], expected_triggered_issues=[ "CheckValue MUST have a human readable name string." ], @@ -177,7 +177,7 @@ def test_5src_check_value_has_action_status_with_not_allowed_value(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["CheckValue"], + expected_triggered_requirements=["CheckValue MUST have an allowed action status"], expected_triggered_issues=[ "`CheckValue` --> `actionStatus` MUST have one of the allowed values." ], @@ -206,7 +206,7 @@ def test_5src_root_data_entity_does_not_mention_check_value_entity(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["Root data entity SHOULD mention CheckValue"], expected_triggered_issues=[ "RootDataEntity SHOULD mention a check value object." ], @@ -235,7 +235,7 @@ def test_5src_check_value_object_does_not_point_to_root_data_entity(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["CheckValue"], + expected_triggered_requirements=["CheckValue SHOULD reference the root data entity"], expected_triggered_issues=[ "`CheckValue` --> `object` SHOULD point to the root of the RO-Crate" ], @@ -267,7 +267,7 @@ def test_5src_check_value_instrument_does_not_point_to_entity_with_type_defined_ rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["CheckValue"], + expected_triggered_requirements=["CheckValue SHOULD reference a DefinedTerm instrument"], expected_triggered_issues=[ "`CheckValue` --> `instrument` SHOULD point to an entity typed `DefinedTerm`" ], @@ -295,7 +295,7 @@ def test_5src_check_value_does_not_have_end_time(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["CheckValue"], + expected_triggered_requirements=["CheckValue SHOULD have an end time"], expected_triggered_issues=["`CheckValue` SHOULD have the `endTime` property."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -321,7 +321,7 @@ def test_5src_check_value_does_not_have_action_status_property(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["CheckValue"], + expected_triggered_requirements=["CheckValue SHOULD have an action status"], expected_triggered_issues=["CheckValue SHOULD have actionStatus property."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -347,7 +347,7 @@ def test_5src_check_value_does_not_point_to_an_agent(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["CheckValue"], + expected_triggered_requirements=["CheckValue SHOULD identify the agent performing the check"], expected_triggered_issues=[ "`CheckValue` --> `agent` SHOULD reference the agent who initiated the check" ], diff --git a/tests/integration/profiles/five-safes-crate/test_5src_13_validation_phase.py b/tests/integration/profiles/five-safes-crate/test_5src_13_validation_phase.py index c19e54663..5a88c9933 100644 --- a/tests/integration/profiles/five-safes-crate/test_5src_13_validation_phase.py +++ b/tests/integration/profiles/five-safes-crate/test_5src_13_validation_phase.py @@ -47,7 +47,7 @@ def test_5src_validation_check_not_of_type_assess_action(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["ValidationCheck"], + expected_triggered_requirements=["ValidationCheck MUST have a name and AssessAction type"], expected_triggered_issues=["ValidationCheck MUST be a `AssessAction`."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -74,7 +74,7 @@ def test_5src_validation_check_name_not_a_string(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["ValidationCheck"], + expected_triggered_requirements=["ValidationCheck MUST have a name and AssessAction type"], expected_triggered_issues=["ValidationCheck MUST have a human readable name string."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -99,7 +99,7 @@ def test_5src_validation_check_has_action_status_with_not_allowed_value(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["ValidationCheck"], + expected_triggered_requirements=["ValidationCheck MUST have an allowed action status"], expected_triggered_issues=[ ( "actionStatus MUST be either PotentialActionStatus, ActiveActionStatus, " @@ -197,7 +197,7 @@ def test_5src_root_data_entity_does_not_mention_validation_check_entity(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["Root data entity SHOULD mention ValidationCheck"], expected_triggered_issues=["RootDataEntity SHOULD mention a ValidationCheck object."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -224,7 +224,7 @@ def test_5src_validation_check_object_does_not_point_to_root_data_entity(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["ValidationCheck"], + expected_triggered_requirements=["ValidationCheck SHOULD reference the root data entity"], expected_triggered_issues=["`ValidationCheck` --> `object` SHOULD point to the root of the RO-Crate"], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -249,7 +249,7 @@ def test_5src_validation_check_instrument_does_not_point_to_5scrate_0p4(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["ValidationCheck"], + expected_triggered_requirements=["ValidationCheck SHOULD reference the Five Safes profile as instrument"], expected_triggered_issues=[ "`ValidationCheck` --> `instrument` SHOULD point to an entity with @id https://w3id.org/5s-crate/0.4" ], @@ -276,7 +276,7 @@ def test_5src_Validation_check_does_not_have_action_status_property(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["ValidationCheck"], + expected_triggered_requirements=["ValidationCheck SHOULD have an action status"], expected_triggered_issues=["ValidationCheck SHOULD have actionStatus property."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -301,7 +301,7 @@ def test_5src_download_action_does_not_have_end_time(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["ValidationCheck"], + expected_triggered_requirements=["ValidationCheck SHOULD have an end time when ended"], expected_triggered_issues=[ ( "ValidationCheck SHOULD have the `endTime` property if `actionStatus` " diff --git a/tests/integration/profiles/five-safes-crate/test_5src_14_workflow_retrieval_phase.py b/tests/integration/profiles/five-safes-crate/test_5src_14_workflow_retrieval_phase.py index c7d92d0cd..e7aed54b6 100644 --- a/tests/integration/profiles/five-safes-crate/test_5src_14_workflow_retrieval_phase.py +++ b/tests/integration/profiles/five-safes-crate/test_5src_14_workflow_retrieval_phase.py @@ -44,7 +44,7 @@ def test_5src_download_action_does_not_have_name(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["DownloadAction"], + expected_triggered_requirements=["DownloadAction MUST have a descriptive name"], expected_triggered_issues=["DownloadAction MUST have a human readable name string."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -71,7 +71,7 @@ def test_5src_download_action_name_not_a_string(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["DownloadAction"], + expected_triggered_requirements=["DownloadAction MUST have a descriptive name"], expected_triggered_issues=["DownloadAction MUST have a human readable name string."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -167,7 +167,7 @@ def test_5src_downloaded_workflow_same_as_is_not_the_same_as_root_data_entity_ma rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["Downloaded Workflow"], + expected_triggered_requirements=["Downloaded workflow sameAs MUST match the root mainEntity"], expected_triggered_issues=[ ( "The property `sameAs` of the entity representing the downloaded workflow " @@ -200,7 +200,7 @@ def test_5src_downloaded_workflow_distribution_is_not_the_same_as_download_actio rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["Downloaded Workflow"], + expected_triggered_requirements=["Downloaded workflow distribution MUST match the download object"], expected_triggered_issues=[ ("DownloadedWorkflow --> `distribution` MUST reference the same entity as `DownloadAction` --> `object`.") ], @@ -230,7 +230,7 @@ def test_5src_download_action_has_action_status_with_not_allowed_value(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["DownloadAction"], + expected_triggered_requirements=["DownloadAction MUST have an allowed action status"], expected_triggered_issues=[ ( "The value of actionStatus MUST be one of the allowed values: " @@ -263,7 +263,7 @@ def test_5src_download_action_is_not_present(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["DownloadAction SHOULD exist"], expected_triggered_issues=["An entity typed DownloadAction SHOULD exist."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -287,7 +287,7 @@ def test_5src_root_data_entity_does_not_mention_download_action_entity(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["Root data entity SHOULD mention DownloadAction when present"], expected_triggered_issues=["RootDataEntity SHOULD mention DownloadAction if this exists."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -312,7 +312,7 @@ def test_5src_download_action_does_not_have_end_time(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["DownloadAction"], + expected_triggered_requirements=["DownloadAction SHOULD have an end time when ended"], expected_triggered_issues=[ ( "`DownloadAction` SHOULD have the `endTime` property " @@ -342,7 +342,7 @@ def test_5src_download_action_does_not_have_action_status_property(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["DownloadAction"], + expected_triggered_requirements=["DownloadAction SHOULD have an action status"], expected_triggered_issues=["`DownloadAction` SHOULD have `actionStatus` property."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -371,7 +371,7 @@ def test_5src_downloaded_workflow_is_not_represented_by_its_own_entity(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.OPTIONAL, expected_validation_result=False, - expected_triggered_requirements=["DownloadAction"], + expected_triggered_requirements=["DownloadAction result MAY reference an existing workflow entity"], expected_triggered_issues=[ ( "The entity representing the downloaded workflow is not defined, " @@ -402,7 +402,7 @@ def test_5src_download_action_does_not_have_start_time(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.OPTIONAL, expected_validation_result=False, - expected_triggered_requirements=["DownloadAction"], + expected_triggered_requirements=["DownloadAction MAY have a start time when begun"], expected_triggered_issues=[ ( "`DownloadAction` MAY have the `startTime` property if `actionStatus` " diff --git a/tests/integration/profiles/five-safes-crate/test_5src_1_root_data_entity_metadata.py b/tests/integration/profiles/five-safes-crate/test_5src_1_root_data_entity_metadata.py index 19e3f21fe..6a3b4600f 100644 --- a/tests/integration/profiles/five-safes-crate/test_5src_1_root_data_entity_metadata.py +++ b/tests/integration/profiles/five-safes-crate/test_5src_1_root_data_entity_metadata.py @@ -46,7 +46,7 @@ def test_5src_root_data_entity_without_source_organization(): rocrate_path=ValidROC().five_safes_crate_request, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["Root data entity MUST identify its source organization"], expected_triggered_issues=["""The Root Data Entity MUST have a `sourceOrganization` property."""], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -79,7 +79,7 @@ def test_5src_root_data_entity_source_organization_not_organization(): rocrate_path=ValidROC().five_safes_crate_request, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["Root data entity MUST identify its source organization"], expected_triggered_issues=[ """The `sourceOrganization` property of the RootDataEntity MUST point to a Project entity.""" ], diff --git a/tests/integration/profiles/five-safes-crate/test_5src_6_workflow_reference.py b/tests/integration/profiles/five-safes-crate/test_5src_6_workflow_reference.py index 2aacf26e3..98dd6b233 100644 --- a/tests/integration/profiles/five-safes-crate/test_5src_6_workflow_reference.py +++ b/tests/integration/profiles/five-safes-crate/test_5src_6_workflow_reference.py @@ -48,7 +48,7 @@ def test_5src_root_data_entity_two_main_entities(): rocrate_path=ValidROC().five_safes_crate_request, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["Root data entity MUST reference a workflow crate"], expected_triggered_issues=["The RootDataEntity MUST have exactly one mainEntity property that is an IRI."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -75,7 +75,7 @@ def test_5src_root_data_entity_no_main_entity(): rocrate_path=ValidROC().five_safes_crate_request, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["Root data entity MUST reference a workflow crate"], expected_triggered_issues=["The RootDataEntity MUST have exactly one mainEntity property that is an IRI."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -108,7 +108,7 @@ def test_5src_root_data_entity_main_entity_not_dataset_iri(): rocrate_path=ValidROC().five_safes_crate_request, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["Root data entity MUST reference a workflow crate"], expected_triggered_issues=["The mainEntity pointed to by the RootDataEntity MUST be of type Dataset"], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, diff --git a/tests/integration/profiles/five-safes-crate/test_5src_7_requesting_workflow_run.py b/tests/integration/profiles/five-safes-crate/test_5src_7_requesting_workflow_run.py index c2a82ba3e..4878d3a10 100644 --- a/tests/integration/profiles/five-safes-crate/test_5src_7_requesting_workflow_run.py +++ b/tests/integration/profiles/five-safes-crate/test_5src_7_requesting_workflow_run.py @@ -50,7 +50,7 @@ def test_rocrate_does_not_have_createaction(): rocrate_path=ValidROC().five_safes_crate_request, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["RootDataEntity", "Root data entity MUST mention a workflow run"], expected_triggered_issues=[ "`RootDataEntity` MUST reference at least one `CreateAction`", " (corresponding to the workflow run) through `mentions`", @@ -86,7 +86,7 @@ def test_rootdataentity_does_not_have_mentions_property(): rocrate_path=ValidROC().five_safes_crate_request, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["Root data entity MUST mention a workflow run"], expected_triggered_issues=[ "`RootDataEntity` MUST reference at least one `CreateAction`", " (corresponding to the workflow run) through `mentions`", @@ -120,7 +120,7 @@ def test_rootdataentity_does_not_mention_create_action(): rocrate_path=ValidROC().five_safes_crate_request, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["Root data entity MUST mention a workflow run"], expected_triggered_issues=[ "`RootDataEntity` MUST reference at least one `CreateAction`", " (corresponding to the workflow run) through `mentions`", @@ -191,7 +191,7 @@ def test_createaction_does_not_have_object_property(): rocrate_path=ValidROC().five_safes_crate_request, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["WorkflowRunAction"], + expected_triggered_requirements=["Workflow run SHOULD have an object"], expected_triggered_issues=[ "`CreateAction` (corresponding to the workflow run) SHOULD have the property `object` with IRI values." ], diff --git a/tests/integration/profiles/five-safes-crate/test_5src_8_disclosure_phase.py b/tests/integration/profiles/five-safes-crate/test_5src_8_disclosure_phase.py index 8113a5111..6f70ff13e 100644 --- a/tests/integration/profiles/five-safes-crate/test_5src_8_disclosure_phase.py +++ b/tests/integration/profiles/five-safes-crate/test_5src_8_disclosure_phase.py @@ -45,7 +45,7 @@ def test_5src_disclosure_object_with_no_name(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["DisclosureCheck"], + expected_triggered_requirements=["DisclosureCheck MUST have a name and AssessAction type"], expected_triggered_issues=["`DisclosureCheck` MUST have a name string of at least 10 characters."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -74,7 +74,7 @@ def test_5src_disclosure_object_with_name_not_string(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["DisclosureCheck"], + expected_triggered_requirements=["DisclosureCheck MUST have a name and AssessAction type"], expected_triggered_issues=["`DisclosureCheck` MUST have a name string of at least 10 characters."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -103,7 +103,7 @@ def test_5src_disclosure_object_with_not_long_enough_name(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["DisclosureCheck"], + expected_triggered_requirements=["DisclosureCheck MUST have a name and AssessAction type"], expected_triggered_issues=["`DisclosureCheck` MUST have a name string of at least 10 characters."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -131,7 +131,7 @@ def test_5src_disclosure_object_not_an_assess_action(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["DisclosureCheck"], + expected_triggered_requirements=["DisclosureCheck MUST have a name and AssessAction type"], expected_triggered_issues=["`DisclosureCheck` MUST be a `AssessAction`."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -159,7 +159,7 @@ def test_5src_disclosure_object_with_no_proper_action_status(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.REQUIRED, expected_validation_result=False, - expected_triggered_requirements=["DisclosureCheck"], + expected_triggered_requirements=["DisclosureCheck MUST have an allowed action status"], expected_triggered_issues=[ ( "`DisclosureCheck` MUST have an actionStatus with an allowed value " @@ -257,7 +257,7 @@ def test_5src_disclosure_object_not_mentioned_by_root_data_entity(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["RootDataEntity"], + expected_triggered_requirements=["Root data entity SHOULD mention DisclosureCheck"], expected_triggered_issues=["`RootDataEntity` SHOULD mention a disclosure object."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -282,7 +282,7 @@ def test_5src_disclosure_object_with_no_action_status(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["DisclosureCheck"], + expected_triggered_requirements=["DisclosureCheck SHOULD have an action status"], expected_triggered_issues=["The `DisclosureCheck` SHOULD have `actionStatus` property."], profile_identifier="five-safes-crate", rocrate_entity_mod_sparql=sparql, @@ -312,7 +312,7 @@ def test_5src_disclosure_object_has_no_end_time_if_ended(): rocrate_path=ValidROC().five_safes_crate_result, requirement_severity=Severity.RECOMMENDED, expected_validation_result=False, - expected_triggered_requirements=["DisclosureCheck"], + expected_triggered_requirements=["DisclosureCheck SHOULD have an end time when ended"], expected_triggered_issues=[ ( "`DisclosureCheck` SHOULD have the `endTime` property if `actionStatus` " diff --git a/tests/integration/profiles/ro-crate-1.2/test_detached_rocrates.py b/tests/integration/profiles/ro-crate-1.2/test_detached_rocrates.py index b340809c4..f12723036 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_detached_rocrates.py +++ b/tests/integration/profiles/ro-crate-1.2/test_detached_rocrates.py @@ -91,13 +91,13 @@ def test_invalid_root_data_entity_identifier_when_online_available(): False, profile_identifier="ro-crate-1.2", skip_checks=[ - "ro-crate-1.2_41.1", - "ro-crate-1.2_41.2", - "ro-crate-1.2_41.3", "ro-crate-1.2_42.1", + "ro-crate-1.2_42.2", + "ro-crate-1.2_42.3", "ro-crate-1.2_43.1", - "ro-crate-1.2_43.2", "ro-crate-1.2_44.1", + "ro-crate-1.2_44.2", + "ro-crate-1.2_45.1", ], expected_triggered_requirements=["Root Data Entity: RECOMMENDED identifier"], expected_triggered_issues=["In a remote RO-Crate, the Root Data Entity @id SHOULD be an absolute URL"], diff --git a/tests/integration/profiles/ro-crate-1.2/test_metadata_contextualEntities.py b/tests/integration/profiles/ro-crate-1.2/test_metadata_contextualEntities.py index 3c16f8361..c9d7f64e4 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_metadata_contextualEntities.py +++ b/tests/integration/profiles/ro-crate-1.2/test_metadata_contextualEntities.py @@ -25,14 +25,14 @@ # Generic RECOMMENDED checks that fire on minimal test crates regardless of the # contextual-entity-specific property being tested. _GENERIC_RECOMMENDED_SKIP = [ - "ro-crate-1.2_48.1", # Root Data Entity: RECOMMENDED funder - "ro-crate-1.2_55.1", # Root Data Entity: RECOMMENDED publisher + "ro-crate-1.2_49.1", # Root Data Entity: RECOMMENDED funder + "ro-crate-1.2_56.1", # Root Data Entity: RECOMMENDED publisher ] # Correct IDs for funder/publisher checks (used in person entity tests). _PERSON_VALID_SKIP = [ - "ro-crate-1.2_48.1", # Root Data Entity: RECOMMENDED funder - "ro-crate-1.2_55.1", # Root Data Entity: RECOMMENDED publisher + "ro-crate-1.2_49.1", # Root Data Entity: RECOMMENDED funder + "ro-crate-1.2_56.1", # Root Data Entity: RECOMMENDED publisher ] diff --git a/tests/integration/profiles/ro-crate-1.2/test_metadata_dataEntities.py b/tests/integration/profiles/ro-crate-1.2/test_metadata_dataEntities.py index 56066bc2a..0e5c96263 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_metadata_dataEntities.py +++ b/tests/integration/profiles/ro-crate-1.2/test_metadata_dataEntities.py @@ -72,8 +72,8 @@ def test_invalid_detached_rocrate_dataEntities(): models.Severity.REQUIRED, False, profile_identifier="ro-crate-1.2", - expected_triggered_requirements=["Data Entity: identifier requirements"], - expected_triggered_issues=["has a local identifier but the Root Data Entity does not have a local identifier"], + expected_triggered_requirements=["Detached RO-Crate: data entities MUST be web-based"], + expected_triggered_issues=["MUST have an absolute URL as @id"], ) diff --git a/tests/integration/profiles/ro-crate-1.2/test_metadata_descriptor.py b/tests/integration/profiles/ro-crate-1.2/test_metadata_descriptor.py index eb7c5bc3b..92ddc0b1a 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_metadata_descriptor.py +++ b/tests/integration/profiles/ro-crate-1.2/test_metadata_descriptor.py @@ -83,3 +83,19 @@ def test_invalid_recommended_prefix_conformsTo(): "The RO-Crate metadata file descriptor `conformsTo` URI SHOULD start with https://w3id.org/ro/crate/" ], ) + + +def test_invalid_string_conformsTo_is_reported_at_required_severity(): + """A literal conformsTo value must be reported even with explicit profile selection.""" + do_entity_test( + __metadata_descriptor_crates__.valid_single_value_conformsTo, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.2", + rocrate_entity_patch={ + "ro-crate-metadata.json": {"conformsTo": "https://w3id.org/ro/crate/1.2"}, + }, + expected_triggered_issues=[ + "Each RO-Crate Metadata Descriptor `conformsTo` value MUST be a reference object containing `@id`", + ], + ) diff --git a/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py b/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py index 4fe28c60d..c84b39e19 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py +++ b/tests/integration/profiles/ro-crate-1.2/test_metadata_document.py @@ -12,9 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json import logging +from unittest.mock import MagicMock -from rocrate_validator import models +import pytest + +from rocrate_validator import models, services +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import requirement as requirement_module +from rocrate_validator.requirements.shacl import requirements as shacl_requirements_module +from rocrate_validator.rocrate.plain import ROCrateLocalFolder +from rocrate_validator.utils.uri import URI from tests.ro_crates_v1_2 import MetadataDocument, MetadataDocumentFormat from tests.shared import do_entity_test @@ -39,6 +48,73 @@ def test_not_utf8(): ) +def test_utf8_check_is_skipped_for_metadata_dict(monkeypatch, tmp_path): + """Metadata-only validation must not read a file descriptor from the CWD.""" + crate_path = __metadata_document_crates__.valid_context_reference + with (crate_path / "ro-crate-metadata.json").open(encoding="utf-8") as stream: + metadata_dict = json.load(stream) + + descriptor_reads = [] + original_get_file_content = ROCrateLocalFolder.get_file_content + + def track_descriptor_reads(crate, path, binary_mode=True): + if path.name == "ro-crate-metadata.json": + descriptor_reads.append((path, binary_mode)) + return original_get_file_content(crate, path, binary_mode) + + monkeypatch.setattr(ROCrateLocalFolder, "get_file_content", track_descriptor_reads) + monkeypatch.chdir(tmp_path) + do_entity_test( + crate_path, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.2", + metadata_dict=metadata_dict, + metadata_only=True, + ) + assert not descriptor_reads + + +def test_missing_descriptor_does_not_emit_unexpected_warnings(monkeypatch, tmp_path): + """Checks depending on metadata should defer to the descriptor checks.""" + requirement_logger = MagicMock() + shacl_logger = MagicMock() + monkeypatch.setattr(requirement_module, "logger", requirement_logger) + monkeypatch.setattr(shacl_requirements_module, "logger", shacl_logger) + + result = services.validate( + models.ValidationSettings( + rocrate_uri=URI(tmp_path), + profile_identifier="ro-crate-1.2", + requirement_severity=models.Severity.OPTIONAL, + ) + ) + + issues = [issue.message for issue in result.get_issues()] + assert result.passed() is False + assert any( + issue is not None and 'file descriptor "ro-crate-metadata.json" is not present' in issue for issue in issues + ) + assert not any( + issue is not None + and any(message in issue for message in ("Unexpected error", "Error checking", "not in the correct format")) + for issue in issues + ) + assert requirement_logger.warning.call_count == 0 + assert shacl_logger.warning.call_count == 0 + + +def test_missing_descriptor_raises_specific_metadata_error(tmp_path): + """Reading a missing descriptor must preserve its metadata-specific cause.""" + crate = ROCrateLocalFolder(tmp_path) + + with pytest.raises(ROCrateMetadataNotFoundError) as error: + crate.metadata.as_dict() + + assert error.value.path == "ro-crate-metadata.json" + assert isinstance(error.value.__cause__, FileNotFoundError) + + def test_not_json(): """ Test that the metadata document is valid when it is not JSON-LD. @@ -85,7 +161,7 @@ def test_not_compacted(): def test_invalid_context_reference(): """ - Test that the metadata document is valid when it has an invalid context reference. + Test that the metadata document is invalid when it has an invalid context reference. """ do_entity_test( __metadata_document_crates__.invalid_context_reference, @@ -94,8 +170,10 @@ def test_invalid_context_reference(): profile_identifier="ro-crate-1.2", expected_triggered_requirements=["File Descriptor JSON-LD format"], expected_triggered_issues=[ - 'RO-Crate file descriptor "ro-crate-metadata.json" ' - 'does not reference the required context "https://w3id.org/ro/crate/1.2/context"' + ( + 'RO-Crate file descriptor "ro-crate-metadata.json" ' + 'does not reference the required context "https://w3id.org/ro/crate/1.2/context"' + ) ], ) @@ -112,6 +190,21 @@ def test_valid_context_reference(): ) +def test_singleton_entity_property_is_recommended_as_a_single_value(): + """ + Test the RO-Crate 1.2 recommendation that a singleton property array is + represented as a single value in compacted JSON-LD. + """ + do_entity_test( + __metadata_document_crates__.valid_context_reference, + models.Severity.RECOMMENDED, + False, + ["Entity properties compact representation"], + ['property "hasPart" SHOULD be represented as a single value'], + profile_identifier="ro-crate-1.2", + ) + + def test_not_referenced_contextual_entity(): """ Test that the metadata document is not valid @@ -165,8 +258,10 @@ def test_not_described_contextual_entity(): profile_identifier="ro-crate-1.2", expected_triggered_requirements=["Contextual Entity RECOMMENDED description"], expected_triggered_issues=[ - "Contextual entities that are referenced by other entities SHOULD be " - "described in the same @graph, with at least an RDF type specified." + ( + "Contextual entities that are referenced by other entities SHOULD be " + "described in the same @graph, with at least an RDF type specified." + ) ], ) @@ -262,3 +357,144 @@ def test_invalid_named_entity_id_format(): expected_triggered_requirements=["Entity identifier: format recommendations"], expected_triggered_issues=["named local entities SHOULD use a '#'-prefixed @id"], ) + + +# --------------------------------------------------------------------------- +# Schema.org IRIs: http namespace, not https (MUST) — see #188 +# --------------------------------------------------------------------------- + + +def test_valid_schema_org_iri_protocol(): + """ + Crate whose Schema.org IRIs all use the http namespace passes. + """ + do_entity_test( + __metadata_document_crates__.valid_schema_org_iri_protocol, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.2", + ) + + +def test_invalid_schema_org_type_protocol(): + """ + An entity typed with an https Schema.org IRI is reported, naming the http term. + """ + do_entity_test( + __metadata_document_crates__.invalid_schema_org_iri_protocol, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.2", + expected_triggered_requirements=["Schema.org @type protocol"], + expected_triggered_issues=[ + "declares the @type 'https://schema.org/Organization'", + "MUST be typed 'Organization' (i.e. 'http://schema.org/Organization')", + ], + ) + + +def test_schema_org_type_presence_check_accepts_https_namespace(): + """ + The existing Schema.org type recommendation remains a presence check and + must not duplicate the dedicated protocol check. + """ + result = services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(__metadata_document_crates__.invalid_schema_org_iri_protocol), + requirement_severity=models.Severity.RECOMMENDED, + profile_identifier="ro-crate-1.2", + ) + ) + assert any(detail.check.requirement.name == "Schema.org @type protocol" for detail in result.get_issues()) + assert not any( + issue.message == "RO-Crate Metadata Entity SHOULD include at least one Schema.org type" + for issue in result.get_issues() + ) + + +def test_https_schema_org_id_is_allowed(): + """ + An https Schema.org IRI used as an @id remains a valid resource identifier. + """ + do_entity_test( + __metadata_document_crates__.valid_schema_org_iri_protocol, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.2", + rocrate_entity_patch={"#uuid": {"propertyID": {"@id": "https://schema.org/identifier"}}}, + ) + + +def test_https_entity_id_is_not_recommended(): + """ + An entity's own HTTPS Schema.org @id is not a compatibility issue. + """ + with (__metadata_document_crates__.valid_schema_org_iri_protocol / "ro-crate-metadata.json").open( + encoding="utf-8" + ) as stream: + metadata_dict = json.load(stream) + metadata_dict["@graph"].append( + { + "@id": "https://schema.org/StandaloneResource", + "@type": "Thing", + "name": "Standalone Schema.org resource", + } + ) + result = services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(__metadata_document_crates__.valid_schema_org_iri_protocol), + requirement_severity=models.Severity.RECOMMENDED, + profile_identifier="ro-crate-1.2", + metadata_only=True, + metadata_dict=metadata_dict, + ) + ) + assert not any(issue.check.requirement.name == "Schema.org @id compatibility" for issue in result.get_issues()) + + +def test_https_nested_schema_org_id_is_recommended_for_compatibility(): + """ + A nested HTTPS Schema.org @id remains valid but emits a compatibility recommendation. + """ + do_entity_test( + __metadata_document_crates__.valid_schema_org_iri_protocol, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.2", + rocrate_entity_patch={"#uuid": {"propertyID": {"@id": "https://schema.org/identifier"}}}, + expected_triggered_requirements=["Schema.org @id compatibility"], + expected_triggered_issues=[ + "references the Schema.org IRI 'https://schema.org/identifier' through a nested @id", + "SHOULD use 'identifier' (i.e. 'http://schema.org/identifier')", + ], + ) + + +def test_schema_org_protocol_shapes_run_after_context_check(): + """ + The context check runs before the SHACL protocol shapes. A context failure + is reported, while the protocol shapes do not emit unrelated violations. + """ + result = services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(__metadata_document_crates__.invalid_context_reference), + requirement_severity=models.Severity.RECOMMENDED, + profile_identifier="ro-crate-1.2", + ) + ) + + protocol_issues = { + issue.check.requirement.name + for issue in result.get_issues() + if issue.check.requirement.name in {"Schema.org @type protocol", "Schema.org @id compatibility"} + } + assert protocol_issues == set() + assert any( + issue.message + and 'does not reference the required context "https://w3id.org/ro/crate/1.2/context"' in issue.message + for issue in result.get_issues() + ) + assert not any( + detail.check.requirement.name in {"Schema.org @type protocol", "Schema.org @id compatibility"} + for detail in result.skipped_check_details + ) diff --git a/tests/integration/profiles/ro-crate-1.2/test_metadata_entities.py b/tests/integration/profiles/ro-crate-1.2/test_metadata_entities.py index ec619d82d..57bce1c1d 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_metadata_entities.py +++ b/tests/integration/profiles/ro-crate-1.2/test_metadata_entities.py @@ -12,9 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json import logging -from rocrate_validator import models +from rdflib import Graph, Literal, URIRef + +from rocrate_validator import models, services +from rocrate_validator.requirements.shacl.transformers import prepare_data_graph +from rocrate_validator.transformers.validation_candidate_marker import VALIDATION_CANDIDATE_PREDICATE from tests.ro_crates_v1_2 import MetadataEntities from tests.shared import do_entity_test @@ -24,6 +29,17 @@ __metadata_entities__ = MetadataEntities() +def _validate_metadata(rocrate_path, metadata): + return services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(rocrate_path), + requirement_severity=models.Severity.RECOMMENDED, + profile_identifier="ro-crate-1.2", + metadata_dict=metadata, + ) + ) + + def test_valid_recommended_schema_type(): """ Test that the metadata document includes at least one Schema.org type. @@ -76,3 +92,132 @@ def test_invalid_recommended_entity_name_warning(): expected_triggered_requirements=["RO-Crate Metadata Entity: RECOMMENDED properties"], expected_triggered_issues=["Entities SHOULD have a human-readable name"], ) + + +def test_orcid_scheme_reference_is_not_validated_as_an_entity(): + """An identifier scheme referenced through propertyID is only cited.""" + rocrate_path = __metadata_entities__.valid_recommended_schema_type + metadata = json.loads((rocrate_path / "ro-crate-metadata.json").read_text(encoding="utf-8")) + root = next(entity for entity in metadata["@graph"] if entity["@id"] == "./") + root["identifier"] = [root["identifier"], {"@id": "#orcid-reference"}] + metadata["@graph"].append( + { + "@id": "#orcid-reference", + "@type": "PropertyValue", + "name": "ORCID identifier scheme", + "propertyID": {"@id": "https://orcid.org"}, + "value": "ORCID", + } + ) + + result = _validate_metadata(rocrate_path, metadata) + + assert result.passed() + assert not any(issue.violatingEntity == "https://orcid.org" for issue in result.issues) + + +def test_orcid_person_claimed_as_author_remains_in_validation_scope(): + """A person under the same ORCID namespace is claimed, not merely cited.""" + rocrate_path = __metadata_entities__.valid_recommended_schema_type + metadata = json.loads((rocrate_path / "ro-crate-metadata.json").read_text(encoding="utf-8")) + person_id = "https://orcid.org/0009-0000-5074-6239" + root = next(entity for entity in metadata["@graph"] if entity["@id"] == "./") + root["author"] = {"@id": person_id} + metadata["@graph"].append({"@id": person_id, "@type": "Person"}) + + result = _validate_metadata(rocrate_path, metadata) + + assert not result.passed() + assert any( + issue.violatingEntity == person_id and issue.message == "Entities SHOULD have a human-readable name" + for issue in result.get_issues(models.Severity.RECOMMENDED) + ) + + +def test_type_only_blank_node_remains_in_contextual_entity_scope(): + """A blank node asserted by the crate is not an external vocabulary term.""" + rocrate_path = __metadata_entities__.valid_recommended_schema_type + metadata = json.loads((rocrate_path / "ro-crate-metadata.json").read_text(encoding="utf-8")) + metadata["@graph"].append({"@id": "_:type-only-person", "@type": "Person"}) + + result = _validate_metadata(rocrate_path, metadata) + + assert any( + issue.violatingEntity == "type-only-person" + and issue.message == "A Contextual Entity MUST have a `name` property" + for issue in result.get_issues(models.Severity.RECOMMENDED) + ) + assert any( + issue.violatingEntity == "type-only-person" + and issue.message == "Contextual entities SHOULD be referenced by other entities." + for issue in result.get_issues(models.Severity.RECOMMENDED) + ) + + +def test_valid_preprocessing_candidates_fixture_keeps_citations_out_of_scope(): + """Cited-only vocabulary IRIs are ignored while described claims remain valid.""" + rocrate_path = __metadata_entities__.valid_preprocessing_candidates + metadata = json.loads((rocrate_path / "ro-crate-metadata.json").read_text(encoding="utf-8")) + + result = _validate_metadata(rocrate_path, metadata) + + assert result.passed() + cited_only_ids = { + "https://vocab.example.org/identifier-scheme", + "https://vocab.example.org/identifier-vocabulary", + "https://vocab.example.org/formats/uuid", + "https://vocab.example.org/profiles/property-value", + "https://vocab.example.org/properties/uuid", + "https://vocab.example.org/types/CustomEntity", + } + assert not any(issue.violatingEntity in cited_only_ids for issue in result.issues) + + +def test_preprocessing_candidates_fixture_marks_claims_and_descriptions_only(): + """The fixture exercises both positive and cited-only preprocessing paths.""" + rocrate_path = __metadata_entities__.valid_preprocessing_candidates + metadata = (rocrate_path / "ro-crate-metadata.json").read_text(encoding="utf-8") + graph = Graph().parse( + data=metadata, + format="json-ld", + publicID="https://example.org/preprocessing-candidates/ro-crate-metadata.json", + ) + prepared_graph = prepare_data_graph(graph) + + def is_marked(identifier: str) -> bool: + return ( + URIRef(identifier), + VALIDATION_CANDIDATE_PREDICATE, + Literal(True), + ) in prepared_graph + + assert is_marked("https://orcid.org/0009-0000-5074-6239") + assert is_marked("https://vocab.example.org/entities/described") + assert not is_marked("https://vocab.example.org/identifier-scheme") + assert not is_marked("https://vocab.example.org/identifier-vocabulary") + assert not is_marked("https://vocab.example.org/formats/uuid") + assert not is_marked("https://vocab.example.org/profiles/property-value") + assert not is_marked("https://vocab.example.org/properties/uuid") + assert not is_marked("https://vocab.example.org/types/CustomEntity") + assert not is_marked("https://vocab.example.org/property/customPredicate") + + +def test_invalid_preprocessing_candidates_fixture_keeps_incomplete_entities_in_scope(): + """Claimed and type-only entities remain subject to metadata checks.""" + rocrate_path = __metadata_entities__.invalid_preprocessing_candidates + metadata = json.loads((rocrate_path / "ro-crate-metadata.json").read_text(encoding="utf-8")) + author_id = "https://orcid.org/0009-0000-5074-6239" + + result = _validate_metadata(rocrate_path, metadata) + + assert not result.passed() + assert any( + issue.violatingEntity == author_id and issue.message == "Entities SHOULD have a human-readable name" + for issue in result.get_issues(models.Severity.RECOMMENDED) + ) + assert any( + issue.violatingEntity.endswith("#type-only-person") + and issue.message == "Entities SHOULD have a human-readable name" + for issue in result.get_issues(models.Severity.RECOMMENDED) + ) + assert not any(issue.violatingEntity == "https://vocab.example.org/identifier-scheme" for issue in result.issues) diff --git a/tests/integration/profiles/ro-crate-1.2/test_metadata_rootDataEntity.py b/tests/integration/profiles/ro-crate-1.2/test_metadata_rootDataEntity.py index 71bbe5b1a..36dfbd3fa 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_metadata_rootDataEntity.py +++ b/tests/integration/profiles/ro-crate-1.2/test_metadata_rootDataEntity.py @@ -126,7 +126,7 @@ def test_valid_recommended_citeAs_for_resolvable_id(): profile_identifier="ro-crate-1.2", # The test crate focuses on cite-as, not identifier format; # skip the identifier-presence and PropertyValue-approach checks. - skip_checks=["ro-crate-1.2_51.1", "ro-crate-1.2_52.1"], + skip_checks=["ro-crate-1.2_52.1", "ro-crate-1.2_53.1"], ) @@ -215,7 +215,7 @@ def test_valid_recommended_identifier_resolution(monkeypatch): profile_identifier="ro-crate-1.2", # The identifier uses a plain URL string rather than a PropertyValue entity; # that format check is not the focus of this test (resolution is). - skip_checks=["ro-crate-1.2_52.0"], + skip_checks=["ro-crate-1.2_53.0"], ) @@ -381,7 +381,7 @@ def test_valid_required_hasPart_all_data_entities(): models.Severity.REQUIRED, True, profile_identifier="ro-crate-1.2", - skip_checks=["ro-crate-1.2_46.1"], + skip_checks=["ro-crate-1.2_47.1"], ) @@ -397,7 +397,7 @@ def test_invalid_required_hasPart_all_data_entities(): profile_identifier="ro-crate-1.2", expected_triggered_requirements=["Root Data Entity: hasPart MUST reference all Data Entities"], expected_triggered_issues=["MUST reference all Data Entities via hasPart"], - skip_checks=["ro-crate-1.2_46.1"], + skip_checks=["ro-crate-1.2_47.1"], ) @@ -464,13 +464,13 @@ def test_valid_recommended_identifier_if_pid(): skip_checks=[ "ro-crate-1.2_35.1", "ro-crate-1.2_38.1", - "ro-crate-1.2_41.1", - "ro-crate-1.2_41.2", - "ro-crate-1.2_41.3", "ro-crate-1.2_42.1", + "ro-crate-1.2_42.2", + "ro-crate-1.2_42.3", "ro-crate-1.2_43.1", - "ro-crate-1.2_43.2", "ro-crate-1.2_44.1", + "ro-crate-1.2_44.2", + "ro-crate-1.2_45.1", "Root Data Entity: use cite-as for resolvable identifiers", "Root Data Entity: persistent identifier resolution", "Root Data Entity: identifier SHOULD be present if PID exists", diff --git a/tests/integration/profiles/ro-crate-1.2/test_referenced_rocrate.py b/tests/integration/profiles/ro-crate-1.2/test_referenced_rocrate.py index c3b12dff2..58f3f02f7 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_referenced_rocrate.py +++ b/tests/integration/profiles/ro-crate-1.2/test_referenced_rocrate.py @@ -34,7 +34,7 @@ def test_valid_referenced_rocrate(): models.Severity.RECOMMENDED, True, profile_identifier="ro-crate-1.2", - skip_checks=["ro-crate-1.2_40.0", "ro-crate-1.2_45.1", "ro-crate-1.2_70.1", "ro-crate-1.2_73.1"], + skip_checks=["ro-crate-1.2_40.0", "ro-crate-1.2_46.1", "ro-crate-1.2_72.1", "ro-crate-1.2_75.1"], ) @@ -155,7 +155,7 @@ def test_valid_referenced_rocrate_with_identifier(): models.Severity.RECOMMENDED, True, profile_identifier="ro-crate-1.2", - skip_checks=["ro-crate-1.2_40.0", "ro-crate-1.2_45.1", "ro-crate-1.2_70.1", "ro-crate-1.2_73.1"], + skip_checks=["ro-crate-1.2_40.0", "ro-crate-1.2_46.1", "ro-crate-1.2_72.1", "ro-crate-1.2_75.1"], ) @@ -170,7 +170,7 @@ def test_valid_referenced_rocrate_with_relative_path(): models.Severity.RECOMMENDED, True, profile_identifier="ro-crate-1.2", - skip_checks=["ro-crate-1.2_40.0", "ro-crate-1.2_45.1", "ro-crate-1.2_70.1", "ro-crate-1.2_73.1"], + skip_checks=["ro-crate-1.2_40.0", "ro-crate-1.2_46.1", "ro-crate-1.2_72.1", "ro-crate-1.2_75.1"], ) diff --git a/tests/integration/profiles/ro-crate-1.2/test_ro_crate_1_2.py b/tests/integration/profiles/ro-crate-1.2/test_ro_crate_1_2.py index 9675b822f..9f2440f94 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_ro_crate_1_2.py +++ b/tests/integration/profiles/ro-crate-1.2/test_ro_crate_1_2.py @@ -15,7 +15,7 @@ import logging from rocrate_validator import models -from tests.ro_crates import InvalidROCrate12, ValidROCrate12 +from tests.ro_crates_v1_2 import InvalidROCrate12, ValidROCrate12 from tests.shared import do_entity_test logger = logging.getLogger(__name__) diff --git a/tests/integration/profiles/ro-crate-1.2/test_workflows_scripts.py b/tests/integration/profiles/ro-crate-1.2/test_workflows_scripts.py index a16394f0e..bb6e0db0d 100644 --- a/tests/integration/profiles/ro-crate-1.2/test_workflows_scripts.py +++ b/tests/integration/profiles/ro-crate-1.2/test_workflows_scripts.py @@ -29,13 +29,13 @@ _GENERIC_RECOMMENDED_SKIP = [ "ro-crate-1.2_40.0", # RO-Crate Metadata Entity: RECOMMENDED properties (check 0) "ro-crate-1.2_40.1", # RO-Crate Metadata Entity: RECOMMENDED properties (check 1) - "ro-crate-1.2_48.1", # Root Data Entity: recommended funder - "ro-crate-1.2_55.1", # Root Data Entity: recommended publisher - "ro-crate-1.2_62.1", # File Data Entity: RECOMMENDED contentSize - "ro-crate-1.2_63.0", # File: RECOMMENDED conformsTo profile - "ro-crate-1.2_75.1", # Contextual Entity Properties - "ro-crate-1.2_76.1", # Contextual Entity RECOMMENDED description - "ro-crate-1.2_81.2", # License entity: RECOMMENDED properties + "ro-crate-1.2_49.1", # Root Data Entity: recommended funder + "ro-crate-1.2_56.1", # Root Data Entity: recommended publisher + "ro-crate-1.2_63.1", # File Data Entity: RECOMMENDED contentSize + "ro-crate-1.2_64.0", # File: RECOMMENDED conformsTo profile + "ro-crate-1.2_76.1", # Contextual Entity Properties + "ro-crate-1.2_77.1", # Contextual Entity RECOMMENDED description + "ro-crate-1.2_82.2", # License entity: RECOMMENDED properties ] diff --git a/tests/integration/profiles/ro-crate-1.3/test_attached_rocrates_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_attached_rocrates_v1_3.py new file mode 100644 index 000000000..1d2a1df38 --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_attached_rocrates_v1_3.py @@ -0,0 +1,95 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator import models +from tests.ro_crates_v1_3 import AttachedROCrates +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + +__attached_crates__ = AttachedROCrates() + + +def test_preview_not_in_hasPart(): + """ + Test that the metadata document is valid when the preview file + is not included in the `hasPart` property of the Root Data Entity. + """ + do_entity_test( + __attached_crates__.valid_preview_not_in_hasPart, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_preview_not_in_hasPart_warning(): + """ + Test that a warning is triggered when the preview file + is not included in the `hasPart` property of the Root Data Entity. + """ + do_entity_test( + __attached_crates__.invalid_preview_not_in_hasPart, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Preview file descriptor should not be included in `hasPart`"], + expected_triggered_issues=["RO-Crate Website files SHOULD NOT be included in `hasPart`"], + ) + + +def test_root_with_IRI_identifier(): + do_entity_test( + __attached_crates__.valid_relative_root_entity_id, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_root_with_relative_identifier(): + do_entity_test( + __attached_crates__.valid_relative_root_entity_id, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_root_with_invalid_relative_identifier(): + do_entity_test( + __attached_crates__.invalid_relative_root_entity_id, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity identifier restriction"], + expected_triggered_issues=[ + "The Root Data Entity MUST be a `Dataset` (as per `schema.org`) and use an IRI or `./` as identifier" + ], + ) + + +def test_root_with_invalid_non_relative_identifier(): + do_entity_test( + __attached_crates__.invalid_non_relative_root_entity_id, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity identifier restriction"], + expected_triggered_issues=[ + "The Root Data Entity MUST be a `Dataset` (as per `schema.org`) and use an IRI or `./` as identifier" + ], + ) diff --git a/tests/integration/profiles/ro-crate-1.3/test_availability_flags_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_availability_flags_v1_3.py new file mode 100644 index 000000000..82da82cb3 --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_availability_flags_v1_3.py @@ -0,0 +1,212 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator import models, services +from rocrate_validator.utils.http import HttpRequester +from rocrate_validator.utils.uri import URI +from tests.ro_crates_v1_3 import ValidROCrate13 + +logger = logging.getLogger(__name__) + + +valid = ValidROCrate13() + +# Minimal set of JSON-LD context keys needed to pass `check_compaction` +# for the test crates used in this module. +_FAKE_CONTEXT_KEYS = { + "about", + "affiliation", + "author", + "cite-as", + "conformsTo", + "funder", + "contentLocation", + "contentSize", + "contentUrl", + "dateCreated", + "dateModified", + "datePublished", + "description", + "encodingFormat", + "hasPart", + "identifier", + "license", + "name", + "propertyID", + "publisher", + "sdDatePublished", + "url", + "value", + "contactPoint", + "contactType", + "email", +} + + +class _FakeContextResponse: + """Minimal HTTP response that satisfies `FileDescriptorJsonLdFormat` checks.""" + + status_code = 200 + headers = {"Content-Type": "application/ld+json"} + + def json(self): + # Map each known key explicitly so `check_compaction` finds them, and add a + # schema.org default vocabulary so entity *types* (Dataset, File, Person, + # Organization, CreativeWork, ...) expand correctly during validation even + # though they are not listed individually. Without `@vocab`, types do not + # resolve to schema.org and the crate fails REQUIRED structural checks. + context = {k: f"http://schema.org/{k}" for k in _FAKE_CONTEXT_KEYS} + context["@vocab"] = "http://schema.org/" + # `conformsTo` is a Dublin Core term in the real RO-Crate context; the + # file-descriptor checks query `dct:conformsTo`, so it must not fall back + # to the schema.org default vocabulary. + context["conformsTo"] = {"@id": "http://purl.org/dc/terms/conformsTo", "@type": "@id"} + return {"@context": context} + + +def _fake_context_get(url, *args, **kwargs): + """Return a fake JSON-LD context for w3id.org; reject anything else.""" + if "w3id.org" in url: + return _FakeContextResponse() + raise RuntimeError(f"Unexpected GET request in test: {url}") + + +def _validate_with_settings(**kwargs): + return services.validate( + models.ValidationSettings( + rocrate_uri=URI(valid.attached_absolute_root), + profile_identifier="ro-crate-1.3", + requirement_severity=models.Severity.RECOMMENDED, + **kwargs, + ) + ) + + +def _availability_messages(result): + return [ + issue.message + for issue in result.get_issues(models.Severity.RECOMMENDED) + if "Web-based Data Entity" in issue.message + ] + + +def _patch_unavailable(monkeypatch): + """Make every HEAD request fail (simulates unreachable web entities) + and return a fake JSON-LD context for GET requests to avoid proxy errors.""" + + def fake_head(url, *args, **kwargs): + raise RuntimeError("Not downloadable") + + monkeypatch.setattr(HttpRequester(), "head", fake_head) + monkeypatch.setattr(HttpRequester(), "get", _fake_context_get) + + +def test_default_availability_warns(monkeypatch): + _patch_unavailable(monkeypatch) + result = _validate_with_settings() + # REQUIRED checks must pass; the unavailable entity only raises a RECOMMENDED warning + assert result.passed(models.Severity.REQUIRED) + messages = _availability_messages(result) + assert messages, "Expected availability warnings for web-based data entities" + + +def test_creation_time_enforces_availability(monkeypatch): + _patch_unavailable(monkeypatch) + result = _validate_with_settings(creation_time=True) + assert not result.passed() + messages = _availability_messages(result) + assert messages, "Expected availability violations at creation time" + + +def test_enforce_availability_flag(monkeypatch): + _patch_unavailable(monkeypatch) + result = _validate_with_settings(enforce_availability=True) + assert not result.passed() + messages = _availability_messages(result) + assert messages, "Expected availability violations when enforced" + + +def test_skip_availability_check(monkeypatch): + monkeypatch.setattr(HttpRequester(), "get", _fake_context_get) + result = _validate_with_settings(skip_availability_check=True) + assert result.passed() + messages = _availability_messages(result) + assert not messages, "Availability warnings should be skipped" + + +def test_content_size_warning(monkeypatch): + class FakeResponse: + def __init__(self, status_code=200, content_length="10"): + self.status_code = status_code + self.headers = {"Content-Length": content_length} + + def raise_for_status(self): + if self.status_code >= 400: + raise RuntimeError("HTTP error") + + def fake_head(url, *args, **kwargs): + if url.endswith("/file.txt"): + return FakeResponse(content_length="10") + if url.endswith("/content.txt"): + raise RuntimeError("Not downloadable") + return FakeResponse(content_length="10") + + monkeypatch.setattr(HttpRequester(), "head", fake_head) + monkeypatch.setattr(HttpRequester(), "get", _fake_context_get) + + metadata_dict = { + "@context": "https://w3id.org/ro/crate/1.3/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.3"}, + "about": {"@id": "./"}, + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Availability test", + "description": "Content size and contentUrl warnings.", + "license": {"@id": "http://spdx.org/licenses/CC0-1.0"}, + "datePublished": "2024-01-01", + "hasPart": [{"@id": "https://example.org/file.txt"}], + }, + { + "@id": "https://example.org/file.txt", + "@type": "File", + "name": "file.txt", + "description": "Remote file.", + "encodingFormat": "text/plain", + "contentSize": "5", + "contentUrl": "https://example.org/content.txt", + }, + ], + } + + result = services.validate( + models.ValidationSettings( + rocrate_uri=URI("."), + metadata_dict=metadata_dict, + metadata_only=True, + profile_identifier="ro-crate-1.3", + requirement_severity=models.Severity.RECOMMENDED, + ) + ) + + messages = [issue.message for issue in result.get_issues(models.Severity.RECOMMENDED) if issue.message] + assert any("contentSize" in message for message in messages) + assert any("contentUrl" in message for message in messages) diff --git a/tests/integration/profiles/ro-crate-1.3/test_detached_rocrates_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_detached_rocrates_v1_3.py new file mode 100644 index 000000000..22a4b2b24 --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_detached_rocrates_v1_3.py @@ -0,0 +1,99 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator import models +from tests.ro_crates_v1_3 import DetachedROCrates +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +__detached_crates__ = DetachedROCrates() + + +def test_valid_local_descriptor_filename(): + """ + Test that a local descriptor filename is valid in a detached RO-Crate. + """ + do_entity_test( + __detached_crates__.valid_local_descriptor_filename, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=[ + "Web-based Data Entity: REQUIRED availability", + "Web-based Data Entity: RECOMMENDED availability", + "Web-based Data Entity: `contentSize` property", + "Web-based Data Entity: `contentUrl` availability", + ], + ) + + +def test_invalid_local_descriptor_filename(): + """ + Test that a local descriptor filename is invalid in a detached RO-Crate. + """ + do_entity_test( + __detached_crates__.invalid_local_descriptor_filename, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["File Descriptor naming convention"], + expected_triggered_issues=[ + "In a detached RO-Crate, " + "the metadata descriptor filename SHOULD " + "be named according to the convention " + "`{prefix}-ro-crate-metadata.json`" + ], + ) + + +def test_root_data_entity_identifier_when_online_available(): + """ + Test that when the RO-Crate is online available, + the Root Data Entity @id SHOULD be an absolute URL in a detached RO-Crate. + """ + do_entity_test( + __detached_crates__.valid_root_data_entity_identifier_when_online_available, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_35.1", "ro-crate-1.3_38.1"], + ) + + +def test_invalid_root_data_entity_identifier_when_online_available(): + """ + Test that when the RO-Crate is online available, + the Root Data Entity @id SHOULD be an absolute URL in a detached RO-Crate. + """ + do_entity_test( + __detached_crates__.invalid_root_data_entity_identifier_when_online_available, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + skip_checks=[ + "ro-crate-1.3_42.1", + "ro-crate-1.3_42.2", + "ro-crate-1.3_42.3", + "ro-crate-1.3_43.1", + "ro-crate-1.3_44.1", + "ro-crate-1.3_44.2", + "ro-crate-1.3_45.1", + ], + expected_triggered_requirements=["Root Data Entity: RECOMMENDED identifier"], + expected_triggered_issues=["In a remote RO-Crate, the Root Data Entity @id SHOULD be an absolute URL"], + ) diff --git a/tests/integration/profiles/ro-crate-1.3/test_metadata_contextualEntities_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_metadata_contextualEntities_v1_3.py new file mode 100644 index 000000000..9cce3460a --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_metadata_contextualEntities_v1_3.py @@ -0,0 +1,511 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=invalid-name + +import logging + +from rocrate_validator import models +from tests.ro_crates_v1_3 import ContextualEntities +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + +__contextual_entities_crates__ = ContextualEntities() + +# Generic RECOMMENDED checks that fire on minimal test crates regardless of the +# contextual-entity-specific property being tested. +_GENERIC_RECOMMENDED_SKIP = [ + "ro-crate-1.3_49.1", # Root Data Entity: RECOMMENDED funder + "ro-crate-1.3_56.1", # Root Data Entity: RECOMMENDED publisher +] + +# Correct IDs for funder/publisher checks (used in person entity tests). +_PERSON_VALID_SKIP = [ + "ro-crate-1.3_49.1", # Root Data Entity: RECOMMENDED funder + "ro-crate-1.3_56.1", # Root Data Entity: RECOMMENDED publisher +] + + +# --------------------------------------------------------------------------- +# License entity: SHOULD be typed as CreativeWork (SHOULD) +# --------------------------------------------------------------------------- + + +def test_valid_license_entity(): + """ + A license entity with an absolute URL @id, CreativeWork type, name and + description SHOULD pass RECOMMENDED validation. + """ + do_entity_test( + __contextual_entities_crates__.valid_license_entity, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=_GENERIC_RECOMMENDED_SKIP, + ) + + +def test_invalid_license_entity_no_type(): + """ + A license entity missing `CreativeWork` in its @type SHOULD trigger a + RECOMMENDED warning. + """ + do_entity_test( + __contextual_entities_crates__.invalid_license_entity_no_type, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["License entity: SHOULD be typed as CreativeWork"], + expected_triggered_issues=["A License entity SHOULD have `CreativeWork` in its `@type`"], + ) + + +def test_invalid_license_entity_no_url(): + """ + A license entity whose @id is not an absolute HTTP(S) URL SHOULD trigger + a RECOMMENDED warning. + """ + do_entity_test( + __contextual_entities_crates__.invalid_license_entity_no_url, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["License entity: SHOULD have absolute URL @id"], + expected_triggered_issues=["A License entity SHOULD have an absolute HTTP(S) URL as its @id"], + ) + + +# --------------------------------------------------------------------------- +# License entity: name and description SHOULD be present (SHOULD) +# --------------------------------------------------------------------------- + + +def test_invalid_license_entity_no_name(): + """ + A license entity missing the `name` property SHOULD trigger a RECOMMENDED + warning. + """ + do_entity_test( + __contextual_entities_crates__.invalid_license_entity_no_name, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["License entity: RECOMMENDED properties"], + expected_triggered_issues=["License entities SHOULD have a name"], + ) + + +def test_invalid_license_entity_no_description(): + """ + A license entity missing the `description` property SHOULD trigger a + RECOMMENDED warning. + """ + do_entity_test( + __contextual_entities_crates__.invalid_license_entity_no_description, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["License entity: RECOMMENDED properties"], + expected_triggered_issues=["License entities SHOULD have a description"], + ) + + +# --------------------------------------------------------------------------- +# Organization entity: SHOULD have ROR identifier as @id (SHOULD) +# --------------------------------------------------------------------------- + + +def test_valid_organization_entity(): + """ + An Organization entity with a ROR @id, contactPoint referencing ContactPoint, + and author with contactPoint SHOULD pass RECOMMENDED validation. + """ + do_entity_test( + __contextual_entities_crates__.valid_organization_entity, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=_GENERIC_RECOMMENDED_SKIP, + ) + + +def test_invalid_organization_no_ror_id(): + """ + An Organization entity whose @id is not a ROR identifier SHOULD trigger a + RECOMMENDED warning. + """ + do_entity_test( + __contextual_entities_crates__.invalid_organization_no_ror_id, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Organization: SHOULD have ROR identifier"], + expected_triggered_issues=["An Organization entity SHOULD have a ROR identifier as its @id"], + ) + + +# --------------------------------------------------------------------------- +# Organization/Person contactPoint: SHOULD reference ContactPoint entity (SHOULD) +# --------------------------------------------------------------------------- + + +def test_invalid_organization_contactpoint_no_entity(): + """ + An Organization whose contactPoint does not reference a ContactPoint entity + SHOULD trigger a RECOMMENDED warning. + """ + do_entity_test( + __contextual_entities_crates__.invalid_organization_contactpoint_no_entity, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Organization: SHOULD have contactPoint referencing ContactPoint"], + expected_triggered_issues=["An Organization's contactPoint SHOULD reference a ContactPoint contextual entity"], + ) + + +def test_invalid_organization_no_contactpoint(): + """ + An Organization without a contactPoint property (where the author still has + contactPoint so only the Organization-specific check fires). + """ + do_entity_test( + __contextual_entities_crates__.invalid_organization_no_contactpoint, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Organization: SHOULD have contactPoint referencing ContactPoint"], + expected_triggered_issues=["An Organization's contactPoint SHOULD reference a ContactPoint contextual entity"], + ) + + +# --------------------------------------------------------------------------- +# Author/Publisher: at least one SHOULD have contactPoint (SHOULD) +# --------------------------------------------------------------------------- + + +def test_invalid_no_author_publisher_contactpoint(): + """ + When neither the author nor the publisher Person/Organization has a + contactPoint property, the Root Data Entity SHOULD trigger a + RECOMMENDED warning. + """ + do_entity_test( + __contextual_entities_crates__.invalid_no_author_publisher_contactpoint, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: at least one author/publisher SHOULD have contactPoint"], + expected_triggered_issues=[ + "At least one author or publisher Person/Organization SHOULD have a contactPoint property" + ], + ) + + +# --------------------------------------------------------------------------- +# Person entity: SHOULD have ORCID identifier as @id (SHOULD) +# --------------------------------------------------------------------------- + + +def test_valid_person_entity(): + """ + A Person entity with an ORCID @id, contactPoint referencing ContactPoint, + and affiliation referencing Organization SHOULD pass RECOMMENDED validation. + """ + do_entity_test( + __contextual_entities_crates__.valid_person_entity, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=_PERSON_VALID_SKIP, + ) + + +def test_invalid_person_no_orcid(): + """ + A Person entity whose @id is not an ORCID identifier SHOULD trigger a + RECOMMENDED warning. + """ + do_entity_test( + __contextual_entities_crates__.invalid_person_no_orcid, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Person: SHOULD have ORCID identifier"], + expected_triggered_issues=["A Person entity SHOULD have an ORCID identifier as its @id"], + ) + + +def test_invalid_person_affiliation_not_org(): + """ + A Person entity whose affiliation does not reference an Organization + SHOULD trigger a RECOMMENDED warning. + """ + do_entity_test( + __contextual_entities_crates__.invalid_person_affiliation_not_org, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Person: RECOMMENDED affiliation"], + expected_triggered_issues=["Persons SHOULD reference an Organization for affiliation"], + ) + + +# --------------------------------------------------------------------------- +# Any Contextual Entity: @id SHOULD be absolute URI or '#'-prefixed (SHOULD) +# --------------------------------------------------------------------------- + + +def test_valid_contextual_entity_id_format(): + """ + A crate whose contextual entities all use absolute URI permalinks or + '#'-prefixed local identifiers SHOULD pass RECOMMENDED validation. + """ + do_entity_test( + __contextual_entities_crates__.valid_contextual_entity_id_format, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=_PERSON_VALID_SKIP, + ) + + +def test_invalid_bare_contactpoint_id(): + """ + A ContactPoint contextual entity with a bare relative @id (no '#', not an + absolute URI) SHOULD trigger a RECOMMENDED warning. + """ + do_entity_test( + __contextual_entities_crates__.invalid_contextual_entity_bare_contactpoint, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Entity identifier: format recommendations"], + expected_triggered_issues=["named local entities SHOULD use a '#'-prefixed @id"], + ) + + +def test_invalid_bare_propertyvalue_id(): + """ + A PropertyValue contextual entity with a bare relative @id (no '#', not an + absolute URI) SHOULD trigger a RECOMMENDED warning. + """ + do_entity_test( + __contextual_entities_crates__.invalid_contextual_entity_bare_propertyvalue, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Entity identifier: format recommendations"], + expected_triggered_issues=["named local entities SHOULD use a '#'-prefixed @id"], + ) + + +# --------------------------------------------------------------------------- +# SoftwareApplication / ComputerLanguage: MUST have name, url, version (REQUIRED) +# --------------------------------------------------------------------------- + + +def test_valid_software_application(): + """ + A SoftwareApplication contextual entity that declares `name`, `url`, and + `version` MUST pass REQUIRED validation. + """ + do_entity_test( + __contextual_entities_crates__.valid_software_application, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_software_application_no_version(): + """ + A SoftwareApplication contextual entity missing the `version` property + MUST trigger a REQUIRED violation. + """ + do_entity_test( + __contextual_entities_crates__.invalid_software_application_no_version, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["SoftwareApplication or ComputerLanguage: REQUIRED `name`, `url`, `version`"], + expected_triggered_issues=["A SoftwareApplication or ComputerLanguage MUST have a `version` property"], + ) + + +def test_invalid_software_application_no_name(): + """ + A SoftwareApplication contextual entity missing the `name` property MUST + trigger a REQUIRED violation. + """ + do_entity_test( + __contextual_entities_crates__.invalid_software_application_no_name, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["SoftwareApplication or ComputerLanguage: REQUIRED `name`, `url`, `version`"], + expected_triggered_issues=["A SoftwareApplication or ComputerLanguage MUST have a `name` property"], + ) + + +def test_invalid_software_application_no_url(): + """ + A SoftwareApplication contextual entity missing the `url` property MUST + trigger a REQUIRED violation. + """ + do_entity_test( + __contextual_entities_crates__.invalid_software_application_no_url, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["SoftwareApplication or ComputerLanguage: REQUIRED `name`, `url`, `version`"], + expected_triggered_issues=["A SoftwareApplication or ComputerLanguage MUST have a `url` property"], + ) + + +def test_valid_computer_language(): + """ + A ComputerLanguage contextual entity (referenced as `programmingLanguage` + of a Script) that declares `name`, `url`, `version`, and an optional + `alternateName` MUST pass REQUIRED validation. + """ + do_entity_test( + __contextual_entities_crates__.valid_computer_language, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +# --------------------------------------------------------------------------- +# Encoding format entity: MAY include `WebPageElement` when @id is a section +# of a webpage (sh:Info — optional suggestion) [5.8] +# --------------------------------------------------------------------------- + +_ENCODING_FORMAT_MAY_REQUIREMENT = "Encoding format: OPTIONAL `WebPageElement` type for section references" + + +def test_info_encoding_format_no_webpageelement(): + """ + An encoding format entity whose `@id` contains a fragment identifier + (section of a webpage) but whose `@type` does NOT include `WebPageElement` + triggers an `sh:Info` suggestion at OPTIONAL severity (RO-Crate 1.3, § 5.8). + """ + do_entity_test( + __contextual_entities_crates__.info_encoding_format_no_webpageelement, + models.Severity.OPTIONAL, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=[_ENCODING_FORMAT_MAY_REQUIREMENT], + expected_triggered_issues=["MAY include `WebPageElement` in its `@type`"], + ) + + +def test_valid_encoding_format_with_webpageelement(): + """ + An encoding format entity whose `@id` contains a fragment identifier AND + whose `@type` already includes `WebPageElement` does NOT trigger the MAY + suggestion (the optional recommendation is satisfied). + """ + from rocrate_validator import services + + result = services.validate( + { + "rocrate_uri": str(__contextual_entities_crates__.valid_encoding_format_webpageelement), + "profile_identifier": "ro-crate-1.3", + "requirement_severity": models.Severity.OPTIONAL, + } + ) + failed_requirement_names = {issue.check.requirement.name for issue in result.get_issues()} + assert _ENCODING_FORMAT_MAY_REQUIREMENT not in failed_requirement_names, ( + f"The MAY requirement {_ENCODING_FORMAT_MAY_REQUIREMENT!r} should NOT fire " + f"when the encoding format entity already includes `WebPageElement` in its @type" + ) + + +def test_encoding_format_no_fragment_not_triggered(): + """ + An encoding format entity whose `@id` does NOT contain a fragment + identifier is outside the target of the MAY shape; the suggestion must + not fire. + """ + from rocrate_validator import services + + result = services.validate( + { + "rocrate_uri": str(__contextual_entities_crates__.encoding_format_no_fragment), + "profile_identifier": "ro-crate-1.3", + "requirement_severity": models.Severity.OPTIONAL, + } + ) + failed_requirement_names = {issue.check.requirement.name for issue in result.get_issues()} + assert _ENCODING_FORMAT_MAY_REQUIREMENT not in failed_requirement_names, ( + f"The MAY requirement {_ENCODING_FORMAT_MAY_REQUIREMENT!r} should NOT fire " + f"when the encoding format entity @id does not contain a fragment identifier" + ) + + +# --------------------------------------------------------------------------- +# SoftwareApplication / ComputerLanguage: MAY have `alternateName` [5.7] +# --------------------------------------------------------------------------- + +_SOFTWAREAPP_COMPUTERLANG_ALTERNATENAME_REQ = "SoftwareApplication or ComputerLanguage: OPTIONAL `alternateName`" + + +def test_info_software_application_no_alternatename(): + """ + A SoftwareApplication contextual entity without an `alternateName` + property triggers an `sh:Info` suggestion at OPTIONAL severity + (RO-Crate 1.3, § 5.7). The existing `valid` SoftwareApplication crate + (used by `test_valid_software_application` at REQUIRED severity) is + reused here: at REQUIRED it passes, at OPTIONAL the MAY suggestion fires. + """ + do_entity_test( + __contextual_entities_crates__.valid_software_application, + models.Severity.OPTIONAL, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=[_SOFTWAREAPP_COMPUTERLANG_ALTERNATENAME_REQ], + expected_triggered_issues=["MAY declare an `alternateName` property"], + ) + + +def test_valid_computer_language_with_alternatename_no_info(): + """ + A ComputerLanguage contextual entity that already declares an + `alternateName` property does NOT trigger the MAY suggestion — the + optional recommendation is satisfied. + """ + do_entity_test( + __contextual_entities_crates__.valid_computer_language_with_alternatename, + models.Severity.OPTIONAL, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_valid_computer_language_alternatename_passes_at_optional(): + """ + A minimal, fully-specified crate with a + ComputerLanguage declaring `name`, `url`, `version`, and the MAY + `alternateName` passes ALL validation checks including OPTIONAL-level + INFO suggestions. + """ + do_entity_test( + __contextual_entities_crates__.valid_computer_language_with_alternatename, + models.Severity.OPTIONAL, + True, + profile_identifier="ro-crate-1.3", + ) diff --git a/tests/integration/profiles/ro-crate-1.3/test_metadata_dataEntities_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_metadata_dataEntities_v1_3.py new file mode 100644 index 000000000..63b542fde --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_metadata_dataEntities_v1_3.py @@ -0,0 +1,526 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=invalid-name + +import logging + +from rocrate_validator import models +from rocrate_validator.utils.http import HttpRequester +from tests.ro_crates_v1_3 import DataEntities +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +__metadata_root_data_entity_crates__ = DataEntities() + + +def test_valid_local_entity_reference(): + """ + Test that a Data Entity is valid when it references a local file using a relative path in its `@id`. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_local_entity_reference, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_local_entity_reference(): + """ + Test that a Data Entity is invalid when it references a local file using an absolute path in its `@id`. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_local_entity_reference, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Data Entity: identifier requirements"], + expected_triggered_issues=["MUST use a relative @id within the RO-Crate root"], + ) + + +def test_valid_detached_rocrate_dataEntities(): + """ + Test that a Data Entity is valid when it references a remote file using an absolute URI in its `@id`. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_detached_rocrate_dataEntities, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_detached_rocrate_dataEntities(): + """ + Test that a Data Entity is invalid when it references a remote file using a relative path in its `@id`. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_detached_rocrate_dataEntities, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Detached RO-Crate: data entities MUST be web-based"], + expected_triggered_issues=["MUST have an absolute URL as @id"], + ) + + +def test_valid_recommended_properties(): + """ + Test that a Data Entity is valid when it includes recommended properties. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_properties, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_properties(): + """ + Test that a Data Entity is invalid when it lacks recommended properties. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_properties, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Data Entity: RECOMMENDED properties"], + expected_triggered_issues=[ + "Data Entities SHOULD have a `name` property", + "Data Entities SHOULD have a `description` property", + ], + ) + + +def test_valid_recommended_encoding_format(): + """ + Test that a Data Entity is valid when it includes the recommended `encodingFormat` property. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_encoding_format, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_encoding_format(): + """ + Test that a Data Entity is invalid when it includes an invalid value for the recommended `encodingFormat` property. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_encoding_format, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["File Data Entity: RECOMMENDED `encodingFormat` property"], + expected_triggered_issues=["Missing or invalid `encodingFormat` linked to the `File Data Entity`"], + ) + + +# --------------------------------------------------------------------------- +# Web entity @id — downloadability (MUST at creation_time / enforce_availability) +# --------------------------------------------------------------------------- + + +class _ZipResponse: + status_code = 200 + headers = {"Content-Type": "application/zip"} + links = {} + + def raise_for_status(self): + pass + + +class _HtmlResponse: + status_code = 200 + headers = {"Content-Type": "text/html; charset=utf-8"} + links = {} + + def raise_for_status(self): + pass + + +def test_valid_required_web_entity_downloadable(monkeypatch): + """ + Web-based Data Entity whose @id returns a non-HTML Content-Type MUST pass + the availability check when enforce_availability=True. + """ + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _ZipResponse()) + + do_entity_test( + __metadata_root_data_entity_crates__.valid_web_entity_downloadable, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + enforce_availability=True, + ) + + +def test_invalid_required_web_entity_not_downloadable(monkeypatch): + """ + Web-based Data Entity whose @id returns text/html (splash page) MUST fail + the availability check when enforce_availability=True. + """ + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _HtmlResponse()) + + do_entity_test( + __metadata_root_data_entity_crates__.invalid_web_entity_splash_page, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + enforce_availability=True, + expected_triggered_requirements=["Web-based Data Entity: REQUIRED availability"], + expected_triggered_issues=["HTML page"], + ) + + +# --------------------------------------------------------------------------- +# Web entity @id — availability warning (RECOMMENDED, default mode) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_web_entity_downloadable_warning(monkeypatch): + """ + Web-based Data Entity whose @id returns application/zip passes the + RECOMMENDED availability check in default mode. + """ + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _ZipResponse()) + + do_entity_test( + __metadata_root_data_entity_crates__.valid_web_entity_downloadable, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_web_entity_splash_page_warning(monkeypatch): + """ + Web-based Data Entity whose @id returns text/html triggers a RECOMMENDED + warning about a possible splash page in default mode. + """ + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _HtmlResponse()) + + do_entity_test( + __metadata_root_data_entity_crates__.invalid_web_entity_splash_page, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Web-based Data Entity: REQUIRED availability"], + expected_triggered_issues=["HTML page"], + ) + + +# --------------------------------------------------------------------------- +# Web entity contentUrl — downloadability (RECOMMENDED) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_content_url_downloadable(monkeypatch): + """ + Web-based Data Entity whose contentUrl returns application/zip passes the + RECOMMENDED contentUrl availability check. + """ + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _ZipResponse()) + + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_content_url, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_content_url_not_downloadable(monkeypatch): + """ + Web-based Data Entity whose contentUrl returns text/html fails the + RECOMMENDED contentUrl availability check. + """ + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _HtmlResponse()) + + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_content_url, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Web-based Data Entity: REQUIRED availability"], + expected_triggered_issues=["contentUrl", "not directly downloadable"], + ) + + +def test_valid_missing_file_local_path(): + """ + A missing local file that declares localPath, and a deliberately absent + file (#id) that declares localPath, SHOULD both pass the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_missing_file_local_path, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=[ + "ro-crate-1.3_16.1", + "ro-crate-1.3_38.1", + "ro-crate-1.3_17.1", + "ro-crate-1.3_39.0", + "ro-crate-1.3_39.1", + "ro-crate-1.3_18.1", + ], + ) + + +def test_invalid_missing_file_no_local_path(): + """ + A missing local file without localPath and a deliberately absent file (#id) + without localPath SHOULD each trigger a RECOMMENDED warning. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_missing_file_local_path, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_16.1"], + expected_triggered_requirements=["Data Entity: missing file SHOULD use localPath"], + expected_triggered_issues=["localPath"], + ) + + +def test_valid_data_entity_license_divergence(): + """ + A Data Entity with a different license from the Root SHOULD pass + the RECOMMENDED check (the entity is correctly overriding the + default license). + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_data_entity_license_divergence, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_16.1", "ro-crate-1.3_38.1"], + ) + + +def test_invalid_data_entity_redundant_license(): + """ + A Data Entity that declares the same license as the Root SHOULD + still pass validation, but a warning SHOULD be logged about the + redundant license declaration. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_data_entity_license_divergence, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_16.1", "ro-crate-1.3_38.1"], + ) + + +def test_redundant_license_logs_warning(): + """ + When a Data Entity declares the same license as the Root, a warning + message SHOULD be logged (not a validation issue). The validation + result MUST pass, and the warning MUST appear in the log stream. + """ + from rocrate_validator.utils.log import __log_stream__ + + # Clear any previous log output + __log_stream__.truncate(0) + __log_stream__.seek(0) + + do_entity_test( + __metadata_root_data_entity_crates__.invalid_data_entity_license_divergence, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_16.1", "ro-crate-1.3_38.1"], + ) + + log_contents = __log_stream__.getvalue() + assert "redundant" in log_contents.lower(), f"Expected a warning log about redundant license, got:\n{log_contents}" + + +# --------------------------------------------------------------------------- +# File Data Entity — contentSize (SHOULD) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_contentSize(): + """ + A File Data Entity with the recommended `contentSize` property SHOULD pass + the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_contentSize, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_contentSize(): + """ + A File Data Entity without the recommended `contentSize` property SHOULD + fail the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_contentSize, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["File Data Entity: RECOMMENDED contentSize"], + expected_triggered_issues=["contentSize"], + ) + + +# --------------------------------------------------------------------------- +# File Data Entity — conformsTo profile (SHOULD) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_conformsto(): + """ + A File Data Entity whose `conformsTo` references a CreativeWork or Profile + SHOULD pass the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_conformsto, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_conformsto(): + """ + A File Data Entity whose `conformsTo` references a non-Profile/non-CreativeWork + entity SHOULD fail the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_conformsto, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["File: RECOMMENDED `conformsTo` profile"], + expected_triggered_issues=["conformsTo"], + ) + + +# --------------------------------------------------------------------------- +# Web-based File Data Entity — sdDatePublished (SHOULD) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_sdDatePublished(monkeypatch): + """ + A web-based File Data Entity with the recommended `sdDatePublished` property + SHOULD pass the RECOMMENDED check. + """ + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _ZipResponse()) + + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_sdDatePublished, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_sdDatePublished(monkeypatch): + """ + A web-based File Data Entity without the recommended `sdDatePublished` property + SHOULD fail the RECOMMENDED check. + """ + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _ZipResponse()) + + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_sdDatePublished, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Web-based Data Entity: RECOMMENDED properties"], + expected_triggered_issues=["sdDatePublished"], + ) + + +# --------------------------------------------------------------------------- +# 4.3 Dataset (Directory) Data Entity — trailing slash (RECOMMENDED) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_dataset_trailing_slash(): + """ + A local Dataset Data Entity whose @id ends with '/' passes the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_dataset_trailing_slash, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + metadata_only=True, + ) + + +def test_invalid_recommended_dataset_trailing_slash(): + """ + A local Dataset Data Entity whose @id does NOT end with '/' fails the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_dataset_trailing_slash, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + metadata_only=True, + expected_triggered_requirements=["Dataset Data Entity: SHOULD end with trailing slash"], + expected_triggered_issues=["The @id of a local Dataset Data Entity SHOULD end with '/'"], + ) + + +# --------------------------------------------------------------------------- +# 4.3 Dataset (Directory) Data Entity — hasPart (RECOMMENDED) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_dataset_has_part(): + """ + A local Dataset Data Entity that lists its contents via hasPart passes + the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_dataset_has_part, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + metadata_only=True, + ) + + +def test_invalid_recommended_dataset_has_part(): + """ + A local Dataset Data Entity without a hasPart property fails the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_dataset_has_part, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + metadata_only=True, + expected_triggered_requirements=["Dataset Data Entity: SHOULD have hasPart"], + expected_triggered_issues=["Local Dataset Data Entities SHOULD list their contents via `hasPart`"], + ) diff --git a/tests/integration/profiles/ro-crate-1.3/test_metadata_descriptor_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_metadata_descriptor_v1_3.py new file mode 100644 index 000000000..8829df7ea --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_metadata_descriptor_v1_3.py @@ -0,0 +1,82 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator import models +from tests.ro_crates_v1_3 import MetadataDescriptor +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +__metadata_descriptor_crates__ = MetadataDescriptor() + + +def test_valid_single_value_conformsTo(): + """ + Test that the metadata descriptor is valid when the `conformsTo` property + includes a single value. + """ + do_entity_test( + __metadata_descriptor_crates__.valid_single_value_conformsTo, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_single_value_conformsTo(): + """ + Test that the metadata descriptor is invalid when the `conformsTo` property + includes multiple values. + """ + do_entity_test( + __metadata_descriptor_crates__.invalid_single_value_conformsTo, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["RO-Crate Metadata File Descriptor RECOMMENDED conformsTo"], + expected_triggered_issues=["The RO-Crate metadata file descriptor SHOULD have a single `conformsTo` value"], + ) + + +def test_valid_recommended_prefix_conformsTo(): + """ + Test that the metadata descriptor is valid when the `conformsTo` property + includes a value with the recommended prefix. + """ + do_entity_test( + __metadata_descriptor_crates__.valid_recommended_prefix_conformsTo, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_prefix_conformsTo(): + """ + Test that the metadata descriptor is invalid when the `conformsTo` property + includes a value with an incorrect prefix. + """ + do_entity_test( + __metadata_descriptor_crates__.invalid_recommended_prefix_conformsTo, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["RO-Crate Metadata File Descriptor RECOMMENDED conformsTo"], + expected_triggered_issues=[ + "The RO-Crate metadata file descriptor `conformsTo` URI SHOULD start with https://w3id.org/ro/crate/" + ], + ) diff --git a/tests/integration/profiles/ro-crate-1.3/test_metadata_document_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_metadata_document_v1_3.py new file mode 100644 index 000000000..378f615d1 --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_metadata_document_v1_3.py @@ -0,0 +1,500 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import logging +from unittest.mock import MagicMock + +import pytest + +from rocrate_validator import models, services +from rocrate_validator.errors import ROCrateMetadataNotFoundError +from rocrate_validator.models import requirement as requirement_module +from rocrate_validator.requirements.shacl import requirements as shacl_requirements_module +from rocrate_validator.rocrate.plain import ROCrateLocalFolder +from rocrate_validator.utils.uri import URI +from tests.ro_crates_v1_3 import MetadataDocument, MetadataDocumentFormat +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +__metadata_document_crates__ = MetadataDocument() +__metadata_document_format_crates__ = MetadataDocumentFormat() + + +def test_not_utf8(): + """ + Test that the metadata document is valid when it is not UTF-8 encoded. + """ + do_entity_test( + __metadata_document_format_crates__.not_utf8, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["File Descriptor UTF-8 encoding"], + expected_triggered_issues=['RO-Crate file descriptor "ro-crate-metadata.json" is not UTF-8 encoded'], + ) + + +def test_utf8_check_is_skipped_for_metadata_dict(monkeypatch, tmp_path): + """Metadata-only validation must not read a file descriptor from the CWD.""" + crate_path = __metadata_document_crates__.valid_context_reference + with (crate_path / "ro-crate-metadata.json").open(encoding="utf-8") as stream: + metadata_dict = json.load(stream) + + descriptor_reads = [] + original_get_file_content = ROCrateLocalFolder.get_file_content + + def track_descriptor_reads(crate, path, binary_mode=True): + if path.name == "ro-crate-metadata.json": + descriptor_reads.append((path, binary_mode)) + return original_get_file_content(crate, path, binary_mode) + + monkeypatch.setattr(ROCrateLocalFolder, "get_file_content", track_descriptor_reads) + monkeypatch.chdir(tmp_path) + do_entity_test( + crate_path, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + metadata_dict=metadata_dict, + metadata_only=True, + ) + assert not descriptor_reads + + +def test_missing_descriptor_does_not_emit_unexpected_warnings(monkeypatch, tmp_path): + """Checks depending on metadata should defer to the descriptor checks.""" + requirement_logger = MagicMock() + shacl_logger = MagicMock() + monkeypatch.setattr(requirement_module, "logger", requirement_logger) + monkeypatch.setattr(shacl_requirements_module, "logger", shacl_logger) + + result = services.validate( + models.ValidationSettings( + rocrate_uri=URI(tmp_path), + profile_identifier="ro-crate-1.3", + requirement_severity=models.Severity.OPTIONAL, + ) + ) + + issues = [issue.message for issue in result.get_issues()] + assert result.passed() is False + assert any( + issue is not None and 'file descriptor "ro-crate-metadata.json" is not present' in issue for issue in issues + ) + assert not any( + issue is not None + and any(message in issue for message in ("Unexpected error", "Error checking", "not in the correct format")) + for issue in issues + ) + assert requirement_logger.warning.call_count == 0 + assert shacl_logger.warning.call_count == 0 + + +def test_missing_descriptor_raises_specific_metadata_error(tmp_path): + """Reading a missing descriptor must preserve its metadata-specific cause.""" + crate = ROCrateLocalFolder(tmp_path) + + with pytest.raises(ROCrateMetadataNotFoundError) as error: + crate.metadata.as_dict() + + assert error.value.path == "ro-crate-metadata.json" + assert isinstance(error.value.__cause__, FileNotFoundError) + + +def test_not_json(): + """ + Test that the metadata document is valid when it is not JSON-LD. + """ + do_entity_test( + __metadata_document_format_crates__.not_jsonld, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["File Descriptor JSON format"], + expected_triggered_issues=['RO-Crate file descriptor "ro-crate-metadata.json" is not valid JSON'], + ) + + +def test_not_flattened(): + """ + Test that the metadata document is valid when it is not flattened. + """ + do_entity_test( + __metadata_document_format_crates__.not_flattened, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["File Descriptor JSON-LD format"], + expected_triggered_issues=[ + 'RO-Crate file descriptor "ro-crate-metadata.json" is not fully flattened at entity "./"' + ], + ) + + +def test_not_compacted(): + """ + Test that the metadata document is valid when it is not compacted. + """ + do_entity_test( + __metadata_document_format_crates__.not_compacted, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["File Descriptor JSON-LD format"], + expected_triggered_issues=['The 1 occurrence of the "https://schema.org/name" URI cannot be used as a key'], + ) + + +def test_invalid_context_reference(): + """ + Test that the metadata document is invalid when it has an invalid context reference. + """ + do_entity_test( + __metadata_document_crates__.invalid_context_reference, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["File Descriptor JSON-LD format"], + expected_triggered_issues=[ + ( + 'RO-Crate file descriptor "ro-crate-metadata.json" ' + 'does not reference the required context "https://w3id.org/ro/crate/1.3/context"' + ) + ], + ) + + +def test_valid_context_reference(): + """ + Test that the metadata document is valid when it has a valid context reference. + """ + do_entity_test( + __metadata_document_crates__.valid_context_reference, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_singleton_entity_property_is_recommended_as_a_single_value(): + """ + Test the RO-Crate 1.3 recommendation that a singleton property array is + represented as a single value in compacted JSON-LD. + """ + do_entity_test( + __metadata_document_crates__.valid_context_reference, + models.Severity.RECOMMENDED, + False, + ["Entity properties compact representation"], + ['property "hasPart" SHOULD be represented as a single value'], + profile_identifier="ro-crate-1.3", + ) + + +def test_not_referenced_contextual_entity(): + """ + Test that the metadata document is not valid + when it has a contextual entity that is not referenced by any other entity in the graph. + """ + do_entity_test( + __metadata_document_crates__.not_referenced_contextual_entity, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Contextual Entity RECOMMENDED references"], + expected_triggered_issues=["Contextual entities SHOULD be referenced by other entities."], + ) + + +def test_referenced_contextual_entity(): + """ + Test that the metadata document is valid + when it has a contextual entity that is referenced by other entities in the graph. + """ + do_entity_test( + __metadata_document_crates__.valid_referenced_contextual_entity, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_described_contextual_entity(): + """ + Test that the metadata document is valid + when it has a contextual entity that is described in the same graph. + """ + do_entity_test( + __metadata_document_crates__.described_contextual_entity, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_not_described_contextual_entity(): + """ + Test that the metadata document is not valid + when it has a contextual entity that is not described in the same graph. + """ + do_entity_test( + __metadata_document_crates__.not_described_contextual_entity, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Contextual Entity RECOMMENDED description"], + expected_triggered_issues=[ + ( + "Contextual entities that are referenced by other entities SHOULD be " + "described in the same @graph, with at least an RDF type specified." + ) + ], + ) + + +# --------------------------------------------------------------------------- +# @id format: no ../ parent traversal (RECOMMENDED) +# --------------------------------------------------------------------------- + + +def test_valid_no_parent_traversal(): + """ + Crate with clean relative @id paths passes the no-../ check. + """ + do_entity_test( + __metadata_document_crates__.valid_no_parent_traversal, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_no_parent_traversal(): + """ + Crate with an entity @id containing ../ fails the RECOMMENDED check. + """ + do_entity_test( + __metadata_document_crates__.invalid_no_parent_traversal, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Entity identifier: format recommendations"], + expected_triggered_issues=["SHOULD NOT contain '../'"], + ) + + +# --------------------------------------------------------------------------- +# @id format: native UTF-8, not percent-encoded (RECOMMENDED) +# --------------------------------------------------------------------------- + + +def test_valid_utf8_identifiers(): + """ + Crate with native UTF-8 characters in @id passes the encoding check. + """ + do_entity_test( + __metadata_document_crates__.valid_utf8_identifiers, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_utf8_identifiers(): + """ + Crate with percent-encoded non-ASCII bytes in @id fails the RECOMMENDED check. + """ + do_entity_test( + __metadata_document_crates__.invalid_utf8_identifiers, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Entity identifier: format recommendations"], + expected_triggered_issues=["percent-encoded non-ASCII characters"], + ) + + +# --------------------------------------------------------------------------- +# @id format: named contextual entity SHOULD use # prefix (RECOMMENDED) +# --------------------------------------------------------------------------- + + +def test_valid_named_entity_id_format(): + """ + Crate where local Person/Organization entities use '#'-prefixed @id passes. + """ + do_entity_test( + __metadata_document_crates__.valid_named_entity_id_format, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_named_entity_id_format(): + """ + Crate where a local Person entity uses a bare relative @id (no '#') fails. + """ + do_entity_test( + __metadata_document_crates__.invalid_named_entity_id_format, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Entity identifier: format recommendations"], + expected_triggered_issues=["named local entities SHOULD use a '#'-prefixed @id"], + ) + + +# --------------------------------------------------------------------------- +# Schema.org IRIs: http namespace, not https (MUST) — see #188 +# --------------------------------------------------------------------------- + + +def test_valid_schema_org_iri_protocol(): + """ + Crate whose Schema.org IRIs all use the http namespace passes. + """ + do_entity_test( + __metadata_document_crates__.valid_schema_org_iri_protocol, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_schema_org_type_protocol(): + """ + An entity typed with an https Schema.org IRI is reported, naming the http term. + """ + do_entity_test( + __metadata_document_crates__.invalid_schema_org_iri_protocol, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Schema.org @type protocol"], + expected_triggered_issues=[ + "declares the @type 'https://schema.org/Organization'", + "MUST be typed 'Organization' (i.e. 'http://schema.org/Organization')", + ], + ) + + +def test_schema_org_type_presence_check_accepts_https_namespace(): + """ + The existing Schema.org type recommendation remains a presence check and + must not duplicate the dedicated protocol check. + """ + result = services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(__metadata_document_crates__.invalid_schema_org_iri_protocol), + requirement_severity=models.Severity.RECOMMENDED, + profile_identifier="ro-crate-1.3", + ) + ) + assert any(detail.check.requirement.name == "Schema.org @type protocol" for detail in result.get_issues()) + assert not any( + issue.message == "RO-Crate Metadata Entity SHOULD include at least one Schema.org type" + for issue in result.get_issues() + ) + + +def test_https_schema_org_id_is_allowed(): + """ + An https Schema.org IRI used as an @id remains a valid resource identifier. + """ + do_entity_test( + __metadata_document_crates__.valid_schema_org_iri_protocol, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + rocrate_entity_patch={"#uuid": {"propertyID": {"@id": "https://schema.org/identifier"}}}, + ) + + +def test_https_entity_id_is_not_recommended(): + """ + An entity's own HTTPS Schema.org @id is not a compatibility issue. + """ + with (__metadata_document_crates__.valid_schema_org_iri_protocol / "ro-crate-metadata.json").open( + encoding="utf-8" + ) as stream: + metadata_dict = json.load(stream) + metadata_dict["@graph"].append( + { + "@id": "https://schema.org/StandaloneResource", + "@type": "Thing", + "name": "Standalone Schema.org resource", + } + ) + result = services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(__metadata_document_crates__.valid_schema_org_iri_protocol), + requirement_severity=models.Severity.RECOMMENDED, + profile_identifier="ro-crate-1.3", + metadata_only=True, + metadata_dict=metadata_dict, + ) + ) + assert not any(issue.check.requirement.name == "Schema.org @id compatibility" for issue in result.get_issues()) + + +def test_https_nested_schema_org_id_is_recommended_for_compatibility(): + """ + A nested HTTPS Schema.org @id remains valid but emits a compatibility recommendation. + """ + do_entity_test( + __metadata_document_crates__.valid_schema_org_iri_protocol, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + rocrate_entity_patch={"#uuid": {"propertyID": {"@id": "https://schema.org/identifier"}}}, + expected_triggered_requirements=["Schema.org @id compatibility"], + expected_triggered_issues=[ + "references the Schema.org IRI 'https://schema.org/identifier' through a nested @id", + "SHOULD use 'identifier' (i.e. 'http://schema.org/identifier')", + ], + ) + + +def test_schema_org_protocol_shapes_run_after_context_check(): + """ + The context check runs before the SHACL protocol shapes. A context failure + is reported, while the protocol shapes do not emit unrelated violations. + """ + result = services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(__metadata_document_crates__.invalid_context_reference), + requirement_severity=models.Severity.RECOMMENDED, + profile_identifier="ro-crate-1.3", + ) + ) + + protocol_issues = { + issue.check.requirement.name + for issue in result.get_issues() + if issue.check.requirement.name in {"Schema.org @type protocol", "Schema.org @id compatibility"} + } + assert protocol_issues == set() + assert any( + issue.message + and 'does not reference the required context "https://w3id.org/ro/crate/1.3/context"' in issue.message + for issue in result.get_issues() + ) + assert not any( + detail.check.requirement.name in {"Schema.org @type protocol", "Schema.org @id compatibility"} + for detail in result.skipped_check_details + ) diff --git a/tests/integration/profiles/ro-crate-1.3/test_metadata_entities_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_metadata_entities_v1_3.py new file mode 100644 index 000000000..29f2e44e2 --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_metadata_entities_v1_3.py @@ -0,0 +1,78 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator import models +from tests.ro_crates_v1_3 import MetadataEntities +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +__metadata_entities__ = MetadataEntities() + + +def test_valid_recommended_schema_type(): + """ + Test that the metadata document includes at least one Schema.org type. + """ + do_entity_test( + __metadata_entities__.valid_recommended_schema_type, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_schema_type_warning(): + """ + Test that a warning is triggered when the metadata document + does not include at least one Schema.org type. + """ + do_entity_test( + __metadata_entities__.invalid_recommended_schema_type, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["RO-Crate Metadata Entity: RECOMMENDED properties"], + expected_triggered_issues=["RO-Crate Metadata Entity SHOULD include at least one Schema.org type"], + ) + + +def test_valid_recommended_entity_name(): + """ + Test that the metadata document includes a `name` property for at least one entity. + """ + do_entity_test( + __metadata_entities__.valid_recommended_name, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_entity_name_warning(): + """ + Test that a warning is triggered when the metadata document + does not include a `name` property for at least one entity. + """ + do_entity_test( + __metadata_entities__.invalid_recommended_name, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["RO-Crate Metadata Entity: RECOMMENDED properties"], + expected_triggered_issues=["Entities SHOULD have a human-readable name"], + ) diff --git a/tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py new file mode 100644 index 000000000..7d53b6f33 --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_metadata_rootDataEntity_v1_3.py @@ -0,0 +1,645 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=invalid-name + +import logging +from pathlib import Path + +from rdflib import Graph, Literal, Namespace, URIRef +from rdflib.namespace import RDF + +from rocrate_validator import models, services +from rocrate_validator.requirements.shacl.validator import SHACLValidator +from rocrate_validator.utils.http import HttpRequester +from tests.ro_crates_v1_3 import RootDataEntity +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +__metadata_root_data_entity_crates__ = RootDataEntity() + + +def test_root_data_entity_type_target_ignores_preview_about_literal(): + """Core 1.1/1.2 root selection ignores preview about values and supports descriptor suffixes.""" + schema = Namespace("http://schema.org/") + root = URIRef("https://example.org/crate/ro-crate-metadata.json/") + preview = URIRef(f"{root}ro-crate-preview.html") + descriptor_cases = ( + ("1.1", "ro-crate-metadata.json"), + ("1.1", "ro-crate-metadata.jsonld"), + ("1.2", "ro-crate-metadata.json"), + ("1.2", "test-ro-crate-metadata.json"), + ("1.2", "ro-crate-metadata.jsonld"), + ) + + for profile_version, descriptor_name in descriptor_cases: + profile_path = Path(__file__).resolve().parents[4] / "rocrate_validator/profiles/ro-crate" / profile_version + shapes = Graph() + shapes.parse(profile_path / "prefixes.ttl", format="turtle") + shapes.parse(profile_path / "must/2_root_data_entity_metadata.ttl", format="turtle") + + data = Graph() + data.add((URIRef(f"{root}{descriptor_name}"), schema.about, root)) + data.add((preview, schema.about, Literal("./"))) + data.add((root, RDF.type, schema.Dataset)) + data.add((root, RDF.type, URIRef("http://www.w3.org/ns/dx/prof/Profile"))) + data.add((root, schema.name, Literal("Workflow RO-Crate profile"))) + data.add((root, schema.description, Literal("A profile crate."))) + data.add((root, schema.datePublished, Literal("2024-01-01"))) + data.add((root, schema.license, URIRef("https://creativecommons.org/licenses/by/4.0/"))) + + result = SHACLValidator(shapes_graph=shapes).validate(data_graph=data) + + assert result.conforms, f"Core profile {profile_version}, descriptor {descriptor_name}" + + +def test_valid_required_datePublished(): + """ + Test that the Root Data Entity is valid when it includes a `datePublished` property. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_required_datePublished, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_required_datePublished(): + """ + Test that the Root Data Entity is invalid when it does not include a `datePublished` property. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_required_datePublished, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["RO-Crate Root Data Entity REQUIRED properties"], + expected_triggered_issues=[ + "The Root Data Entity MUST have a `datePublished` " + "property (as specified by schema.org) with a valid ISO 8601 date" + ], + ) + + +def test_valid_required_downloadable_citeAs(monkeypatch): + """ + Test that the Root Data Entity is valid when it includes a `cite-as` property + that references a downloadable item (mocked as application/zip via HEAD). + """ + + class _DownloadableResponse: + status_code = 200 + headers = {"Content-Type": "application/zip"} + links = {} + + def raise_for_status(self): + pass + + def _fake_head(url, *args, **kwargs): + logger.debug("Mock HEAD request to %s with args: %s, kwargs: %s", url, args, kwargs) + return _DownloadableResponse() + + monkeypatch.setattr(HttpRequester(), "head", _fake_head) + + do_entity_test( + __metadata_root_data_entity_crates__.valid_required_downloadable_citeAs, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + enforce_availability=True, + ) + + +def test_invalid_required_downloadable_citeAs(monkeypatch): + """ + Test that the Root Data Entity is invalid when it includes a `cite-as` property + that does not reference a downloadable item (mocked as text/html via HEAD). + """ + + class _HtmlResponse: + status_code = 200 + headers = {"Content-Type": "text/html; charset=utf-8"} + links = {} + + def raise_for_status(self): + pass + + def _fake_head(url, *args, **kwargs): + logger.debug("Mock HEAD request to %s with args: %s, kwargs: %s", url, args, kwargs) + return _HtmlResponse() + + monkeypatch.setattr(HttpRequester(), "head", _fake_head) + + do_entity_test( + __metadata_root_data_entity_crates__.invalid_required_downloadable_citeAs, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + enforce_availability=True, + expected_triggered_requirements=["Root Data Entity: `cite-as` downloadability"], + expected_triggered_issues=["MUST ultimately provide the RO-Crate as a downloadable item"], + ) + + +def test_valid_recommended_citeAs_for_resolvable_id(): + """ + Test that the Root Data Entity is valid when it has a resolvable identifier + and includes a `cite-as` property that references the Root Data Entity. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_citeAs_for_resolvable_id, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + # The test crate focuses on cite-as, not identifier format; + # skip the identifier-presence and PropertyValue-approach checks. + skip_checks=["ro-crate-1.3_52.1", "ro-crate-1.3_53.1"], + ) + + +def test_invalid_recommended_citeAs_for_resolvable_id(): + """ + Test that the Root Data Entity is invalid when it has a resolvable identifier + and does not include a `cite-as` property that references the Root Data Entity. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_citeAs_for_resolvable_id, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: use cite-as for resolvable identifiers"], + expected_triggered_issues=[ + "If the Root Data Entity has a resolvable identifier, " + "it SHOULD be included in the `cite-as` property of the RO-Crate Metadata Entity." + ], + ) + + +def test_valid_additional_conformsTo_reference(): + """ + Test that the Root Data Entity is valid when it includes + an additional `conformsTo` property that references a Profile entity. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_additional_conformsTo_reference, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_additional_conformsTo_reference(): + """ + Test that the Root Data Entity is invalid when it includes + an additional `conformsTo` property that does not reference a Profile entity. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_additional_conformsTo_reference, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["RO-Crate Root Data Entity: optional `conformsTo` property value restriction"], + expected_triggered_issues=[ + "If the Root Data Entity includes a `conformsTo` property, its values MUST reference Profile entities." + ], + ) + + +# --------------------------------------------------------------------------- +# Root Data Entity identifier — persistent identifier resolution (RECOMMENDED) +# --------------------------------------------------------------------------- + + +class _ZipResponse: + status_code = 200 + headers = {"Content-Type": "application/zip"} + links = {} + + def raise_for_status(self): + pass + + +class _HtmlResponse: + status_code = 200 + headers = {"Content-Type": "text/html; charset=utf-8"} + links = {} + + def raise_for_status(self): + pass + + +def test_valid_recommended_identifier_resolution(monkeypatch): + """ + Root Data Entity whose identifier URL resolves to a downloadable resource + (mocked as application/zip) passes the RECOMMENDED identifier resolution check. + """ + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _ZipResponse()) + + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_identifier_resolution, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + # The identifier uses a plain URL string rather than a PropertyValue entity; + # that format check is not the focus of this test (resolution is). + skip_checks=["ro-crate-1.3_53.0"], + ) + + +def test_invalid_recommended_identifier_resolution(monkeypatch): + """ + Root Data Entity whose identifier URL returns text/html (landing page) fails + the RECOMMENDED identifier resolution check. + """ + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _HtmlResponse()) + + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_identifier_resolution, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: persistent identifier resolution"], + expected_triggered_issues=["SHOULD resolve to the RO-Crate Metadata Document or an archive"], + ) + + +# --------------------------------------------------------------------------- +# Root Data Entity: publisher SHOULD be present (RECOMMENDED) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_publisher(): + """ + Root Data Entity with a publisher Organization passes the RECOMMENDED + publisher check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_publisher, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_publisher(): + """ + Root Data Entity without a publisher fails the RECOMMENDED publisher check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_publisher, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: recommended publisher"], + expected_triggered_issues=["SHOULD have a `publisher` property"], + ) + + +# --------------------------------------------------------------------------- +# Root Data Entity: funder SHOULD be present (RECOMMENDED — K1/K2/K3) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_funding(): + """ + Root Data Entity with a funder Organization (which itself references an + external funder) passes all RECOMMENDED funding checks. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_funding, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_recommended_funding_no_funder(): + """ + Root Data Entity with no `funder` property fails the RECOMMENDED + direct-funder check (K3). + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_funding_no_funder, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: recommended funder"], + expected_triggered_issues=["SHOULD reference funders directly via the `funder` property"], + ) + + +def test_invalid_recommended_funding_non_org_funder(): + """ + Root Data Entity whose `funder` references a Person (not an Organization) + fails the RECOMMENDED funder-type check (K1). + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_funding_non_org_funder, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Funder entity: recommended Organization type"], + expected_triggered_issues=["SHOULD be of type `Organization`"], + ) + + +def test_invalid_recommended_funding_no_project_funder(): + """ + Root Data Entity whose local project Organization (`funder`) does not itself + reference an external funder fails the RECOMMENDED project-org funder check (K2). + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_funding_no_project_funder, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Project Organization: recommended funder reference"], + expected_triggered_issues=["SHOULD itself reference an external `funder`"], + ) + + +# --------------------------------------------------------------------------- +# Root Data Entity: datePublished SHOULD specify at least day precision +# --------------------------------------------------------------------------- + + +def test_valid_recommended_datePublished_day_precision(): + """ + Root Data Entity with datePublished in YYYY-MM-DD format passes the + RECOMMENDED day precision check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_datePublished_day_precision, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_35.1"], + ) + + +def test_invalid_recommended_datePublished_day_precision(): + """ + Root Data Entity with datePublished as just year (YYYY) fails the + RECOMMENDED day precision check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_datePublished_day_precision, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: datePublished SHOULD specify at least day precision"], + expected_triggered_issues=["SHOULD specify datePublished to at least the precision of a day"], + skip_checks=["ro-crate-1.3_35.1"], + ) + + +# --------------------------------------------------------------------------- +# Root Data Entity: hasPart MUST reference all Data Entities +# --------------------------------------------------------------------------- + + +def test_valid_required_hasPart_all_data_entities(): + """ + Root Data Entity that references all Data Entities via hasPart (directly + or indirectly) passes the REQUIRED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_required_hasPart_all_data_entities, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_47.1"], + ) + + +def test_profile_root_contextual_datasets_are_reported_when_missing_from_hasPart(): + """Dataset references remain Data Entities even when they are contextual to a Profile.""" + result = services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(__metadata_root_data_entity_crates__.profile_root_contextual_datasets), + requirement_severity=models.Severity.REQUIRED, + profile_identifier="ro-crate-1.3", + ) + ) + + has_part_issues = [ + issue + for issue in result.get_issues(models.Severity.REQUIRED) + if issue.check.name == "Root Data Entity: hasPart MUST reference all Data Entities" + ] + + assert {str(issue.violatingPropertyValue) for issue in has_part_issues} == { + "https://w3id.org/ro/crate/1.2", + "https://example.org/referenced-dataset", + } + + +def test_invalid_required_hasPart_all_data_entities(): + """ + Root Data Entity that does NOT reference all Data Entities via hasPart + fails the REQUIRED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_required_hasPart_all_data_entities, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: hasPart MUST reference all Data Entities"], + expected_triggered_issues=["MUST reference all Data Entities via hasPart"], + skip_checks=["ro-crate-1.3_47.1"], + ) + + +def test_invalid_required_hasPart_reports_unreferenced_data_entity(): + """The hasPart check reports the missing Data Entity, not the root itself.""" + result = services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(__metadata_root_data_entity_crates__.invalid_required_hasPart_all_data_entities), + requirement_severity=models.Severity.REQUIRED, + profile_identifier="ro-crate-1.3", + ) + ) + + has_part_issues = [ + issue + for issue in result.get_issues(models.Severity.REQUIRED) + if issue.check.name == "Root Data Entity: hasPart MUST reference all Data Entities" + ] + + assert len(has_part_issues) == 1 + assert str(has_part_issues[0].violatingPropertyValue).endswith("/file2.txt") + + +def test_invalid_hasPart_workflow_not_in_haspart(): + """ + A Workflow File entity (typed as File/SoftwareSourceCode/ComputationalWorkflow) + that is not referenced via hasPart triggers the REQUIRED hasPart check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_hasPart_workflow_not_in_haspart, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: hasPart MUST reference all Data Entities"], + expected_triggered_issues=["MUST reference all Data Entities via hasPart"], + ) + + +def test_invalid_hasPart_web_entity_not_in_haspart(): + """ + A Web Data Entity (absolute URL @id) that is not referenced via hasPart + triggers the REQUIRED hasPart check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_hasPart_web_entity_not_in_haspart, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: hasPart MUST reference all Data Entities"], + expected_triggered_issues=["MUST reference all Data Entities via hasPart"], + ) + + +def test_invalid_hasPart_dataset_not_in_haspart(): + """ + A sub-Dataset Directory Data Entity that is not referenced via hasPart + triggers the REQUIRED hasPart check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_hasPart_dataset_not_in_haspart, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: hasPart MUST reference all Data Entities"], + expected_triggered_issues=["MUST reference all Data Entities via hasPart"], + ) + + +# --------------------------------------------------------------------------- +# Root Data Entity: identifier SHOULD be present if PID exists (RECOMMENDED) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_identifier_if_pid(): + """ + Root Data Entity with absolute URI @id and identifier property passes + the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_identifier_if_pid, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=[ + "ro-crate-1.3_35.1", + "ro-crate-1.3_38.1", + "ro-crate-1.3_42.1", + "ro-crate-1.3_42.2", + "ro-crate-1.3_42.3", + "ro-crate-1.3_43.1", + "ro-crate-1.3_44.1", + "ro-crate-1.3_44.2", + "ro-crate-1.3_45.1", + "Root Data Entity: use cite-as for resolvable identifiers", + "Root Data Entity: persistent identifier resolution", + "Root Data Entity: identifier SHOULD be present if PID exists", + ], + skip_availability_check=True, + ) + + +def test_invalid_recommended_identifier_if_pid(): + """ + Root Data Entity with absolute URI @id but no identifier property fails + the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_identifier_if_pid, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: identifier SHOULD be present if PID exists"], + expected_triggered_issues=["SHOULD have an `identifier` property if it has a persistent identifier"], + ) + + +# --------------------------------------------------------------------------- +# Root Data Entity: identifier SHOULD use PropertyValue approach (RECOMMENDED) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_identifier_propertyvalue(): + """ + Root Data Entity that uses PropertyValue for identifier passes the + RECOMMENDED check per Science On Schema.org. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_identifier_propertyvalue, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_35.1"], + ) + + +def test_invalid_recommended_identifier_propertyvalue(): + """ + Root Data Entity that uses plain string for identifier fails the + RECOMMENDED PropertyValue approach check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_identifier_propertyvalue, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: identifier SHOULD use PropertyValue approach"], + expected_triggered_issues=["SHOULD use PropertyValue entities for identifiers"], + ) + + +# --------------------------------------------------------------------------- +# Root Data Entity: conformsTo SHOULD be present if profiles exist (RECOMMENDED) +# --------------------------------------------------------------------------- + + +def test_valid_recommended_conformsto_if_profiles(): + """ + Root Data Entity that has a Profile entity in the graph and includes + conformsTo on the Root passes the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.valid_recommended_conformsto_if_profiles, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_35.1"], + ) + + +def test_invalid_recommended_conformsto_if_profiles(): + """ + Root Data Entity that has a Profile entity in the graph but does NOT + include conformsTo on the Root fails the RECOMMENDED check. + """ + do_entity_test( + __metadata_root_data_entity_crates__.invalid_recommended_conformsto_if_profiles, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Root Data Entity: conformsTo SHOULD be present if profiles exist"], + expected_triggered_issues=["SHOULD have a `conformsTo` property if the RO-Crate conforms to profiles"], + ) diff --git a/tests/integration/profiles/ro-crate-1.3/test_referenced_rocrate_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_referenced_rocrate_v1_3.py new file mode 100644 index 000000000..66e5c501e --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_referenced_rocrate_v1_3.py @@ -0,0 +1,243 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator import models +from rocrate_validator.utils.http import HttpRequester +from tests.ro_crates_v1_3 import ReferencedROCrates +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + +__referenced_rocrate_crates__ = ReferencedROCrates() + + +def test_valid_referenced_rocrate(): + """ + A crate referencing another RO-Crate with all recommended properties + SHOULD pass RECOMMENDED validation. + """ + do_entity_test( + __referenced_rocrate_crates__.valid, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_40.0", "ro-crate-1.3_46.1", "ro-crate-1.3_72.1", "ro-crate-1.3_75.1"], + ) + + +def test_invalid_referenced_rocrate_no_versionless_conformsto(): + """ + A referenced RO-Crate data entity missing the version-less conformsTo + SHOULD trigger a RECOMMENDED warning. + """ + do_entity_test( + __referenced_rocrate_crates__.invalid_no_versionless_conformsto, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Referenced RO-Crate: SHOULD include version-less profile in conformsTo"], + expected_triggered_issues=["version-less"], + ) + + +def test_invalid_root_conformsto_versionless(): + """ + The Root Data Entity MUST NOT declare the version-less conformsTo profile URI. + """ + do_entity_test( + __referenced_rocrate_crates__.invalid_root_conformsto_versionless, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["RO-Crate Root Data Entity: MUST NOT declare version-less conformsTo profile"], + expected_triggered_issues=["version-less"], + ) + + +def test_invalid_referenced_rocrate_no_subjectof(): + """ + A referenced RO-Crate data entity missing subjectOf SHOULD trigger + a RECOMMENDED warning. + """ + do_entity_test( + __referenced_rocrate_crates__.invalid_no_subjectof, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Referenced RO-Crate: SHOULD have subjectOf"], + expected_triggered_issues=["subjectOf"], + ) + + +def test_invalid_referenced_rocrate_md_encoding_format(): + """ + A referenced RO-Crate metadata descriptor with wrong encodingFormat + SHOULD trigger a RECOMMENDED warning. + """ + do_entity_test( + __referenced_rocrate_crates__.invalid_md_encoding_format, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Referenced RO-Crate metadata descriptor: recommended properties"], + expected_triggered_issues=["encodingFormat"], + ) + + +def test_invalid_referenced_rocrate_md_conformsto(): + """ + A referenced RO-Crate metadata descriptor with conformsTo SHOULD trigger + a RECOMMENDED warning (conformsTo belongs on the data entity). + """ + do_entity_test( + __referenced_rocrate_crates__.invalid_md_conformsto, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Referenced RO-Crate metadata descriptor: recommended properties"], + expected_triggered_issues=["conformsTo"], + ) + + +def test_invalid_referenced_rocrate_md_about(): + """ + A referenced RO-Crate metadata descriptor with about SHOULD trigger + a RECOMMENDED warning (about belongs on the crate's own metadata descriptor). + """ + do_entity_test( + __referenced_rocrate_crates__.invalid_md_about, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Referenced RO-Crate metadata descriptor: recommended properties"], + expected_triggered_issues=["about"], + ) + + +def test_invalid_referenced_rocrate_missing_sddatepublished(): + """ + A referenced RO-Crate data entity whose @id is an absolute URI not declared + as a persistent identifier SHOULD include `sdDatePublished`; omitting it + triggers a RECOMMENDED warning (RO-Crate 1.3, § 4.5). + """ + do_entity_test( + __referenced_rocrate_crates__.invalid_missing_sddatepublished, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=[ + "Referenced RO-Crate: SHOULD have sdDatePublished when @id is an absolute URI without persistent identifier" + ], + expected_triggered_issues=["SHOULD include `sdDatePublished` to indicate when the URI was accessed"], + ) + + +def test_valid_referenced_rocrate_with_identifier(): + """ + A referenced RO-Crate with a declared `identifier` (case #1) does NOT + require `sdDatePublished`; the structural SHACL check should NOT fire. + """ + do_entity_test( + __referenced_rocrate_crates__.valid_with_identifier, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_40.0", "ro-crate-1.3_46.1", "ro-crate-1.3_72.1", "ro-crate-1.3_75.1"], + ) + + +def test_valid_referenced_rocrate_with_relative_path(): + """ + A referenced RO-Crate with a relative path @id (case #2: attached nested + sub-crate) does NOT require `sdDatePublished`; the structural SHACL check + should NOT fire on relative path @ids. + """ + do_entity_test( + __referenced_rocrate_crates__.valid_with_relative_path, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=["ro-crate-1.3_40.0", "ro-crate-1.3_46.1", "ro-crate-1.3_72.1", "ro-crate-1.3_75.1"], + ) + + +def test_invalid_referenced_rocrate_no_cite_as_signposting(monkeypatch): + """ + Python network-aware refinement: when the referenced RO-Crate @id does NOT + declare Signposting `Link: rel="cite-as"`, `sdDatePublished` SHOULD be + present. Mocked HEAD response returns no `cite-as` link. + """ + + class _NoCiteAsResponse: + status_code = 200 + headers = {"Content-Type": "text/html"} + links = {} + + def raise_for_status(self): + pass + + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _NoCiteAsResponse()) + + do_entity_test( + __referenced_rocrate_crates__.invalid_missing_sddatepublished, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + enforce_availability=True, + expected_triggered_requirements=["Referenced RO-Crate: Signposting cite-as refinement for sdDatePublished"], + expected_triggered_issues=['has no Signposting `Link: rel="cite-as"` declared'], + ) + + +def test_valid_referenced_rocrate_with_cite_as_signposting(monkeypatch): + """ + Python network-aware refinement: when the referenced RO-Crate @id DOES + declare Signposting `Link: rel="cite-as"`, `sdDatePublished` is NOT + required; the Python check suppresses its warning. The structural SHACL + warning still fires (it cannot inspect network headers). + """ + + class _CiteAsResponse: + status_code = 200 + headers = {"Content-Type": "text/html"} + links = { + "cite-as": { + "url": "https://doi.org/10.5281/zenodo.1234567", + "rel": "cite-as", + } + } + + def raise_for_status(self): + pass + + monkeypatch.setattr(HttpRequester(), "head", lambda url, **kw: _CiteAsResponse()) + + # Python check passes (cite-as present); SHACL structural shape still + # warns. We verify the Python-specific requirement is NOT triggered. + from rocrate_validator import services + + result = services.validate( + { + "rocrate_uri": str(__referenced_rocrate_crates__.invalid_missing_sddatepublished), + "profile_identifier": "ro-crate-1.3", + "requirement_severity": models.Severity.RECOMMENDED, + "enforce_availability": True, + } + ) + failed_requirement_names = {issue.check.requirement.name for issue in result.get_issues()} + assert "Referenced RO-Crate: Signposting cite-as refinement for sdDatePublished" not in failed_requirement_names, ( + "Python cite-as refinement should NOT fire when cite-as is declared" + ) diff --git a/tests/integration/profiles/ro-crate-1.3/test_ro_crate_1_3.py b/tests/integration/profiles/ro-crate-1.3/test_ro_crate_1_3.py new file mode 100644 index 000000000..12958b18a --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_ro_crate_1_3.py @@ -0,0 +1,103 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator import models +from tests.ro_crates_v1_3 import InvalidROCrate13, ValidROCrate13 +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +paths = InvalidROCrate13() +valid = ValidROCrate13() + + +def test_valid_attached_ro_crate_1_3(): + do_entity_test( + valid.attached, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_valid_attached_ro_crate_absolute_root_id(): + do_entity_test( + valid.attached_absolute_root, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_context_version(): + do_entity_test( + paths.invalid_context, + models.Severity.REQUIRED, + False, + ["File Descriptor JSON-LD format"], + ["does not reference the required context"], + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_date_published_single_value(): + do_entity_test( + paths.invalid_date_published, + models.Severity.REQUIRED, + False, + ["RO-Crate Root Data Entity REQUIRED properties"], + profile_identifier="ro-crate-1.3", + ) + + +def test_valid_detached_ro_crate(): + do_entity_test( + valid.detached, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_valid_detached_prefixed_metadata_filename(): + do_entity_test( + valid.detached_prefixed, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_detached_relative_entity(): + do_entity_test( + paths.detached_relative_entity, + models.Severity.REQUIRED, + False, + ["Detached RO-Crate: data entities MUST be web-based"], + profile_identifier="ro-crate-1.3", + ) + + +def test_detached_bad_filename_recommended(): + do_entity_test( + paths.detached_bad_filename, + models.Severity.RECOMMENDED, + False, + ["File Descriptor naming convention"], + ["metadata descriptor filename"], + profile_identifier="ro-crate-1.3", + ) diff --git a/tests/integration/profiles/ro-crate-1.3/test_workflows_scripts_v1_3.py b/tests/integration/profiles/ro-crate-1.3/test_workflows_scripts_v1_3.py new file mode 100644 index 000000000..5acb753a0 --- /dev/null +++ b/tests/integration/profiles/ro-crate-1.3/test_workflows_scripts_v1_3.py @@ -0,0 +1,324 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator import models +from tests.ro_crates_v1_3 import WorkflowsScripts +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + +__workflows_scripts_crates__ = WorkflowsScripts() + +# General RECOMMENDED checks that fire on minimal test crates regardless of the +# workflow/script-specific property being tested. These are skipped in the +# "valid" RECOMMENDED test cases so the assertion is only about the +# workflow-specific shape. +_GENERIC_RECOMMENDED_SKIP = [ + "ro-crate-1.3_40.0", # RO-Crate Metadata Entity: RECOMMENDED properties (check 0) + "ro-crate-1.3_40.1", # RO-Crate Metadata Entity: RECOMMENDED properties (check 1) + "ro-crate-1.3_49.1", # Root Data Entity: recommended funder + "ro-crate-1.3_56.1", # Root Data Entity: recommended publisher + "ro-crate-1.3_63.1", # File Data Entity: RECOMMENDED contentSize + "ro-crate-1.3_64.0", # File: RECOMMENDED conformsTo profile + "ro-crate-1.3_76.1", # Contextual Entity Properties + "ro-crate-1.3_77.1", # Contextual Entity RECOMMENDED description + "ro-crate-1.3_82.2", # License entity: RECOMMENDED properties +] + + +# --------------------------------------------------------------------------- +# Script type checks (MUST) +# --------------------------------------------------------------------------- + + +def test_valid_script_type(): + """ + A Script entity with `File`, `SoftwareSourceCode` in @type and a `name` + MUST pass REQUIRED validation. + """ + do_entity_test( + __workflows_scripts_crates__.valid_script_type, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_script_type(): + """ + A Script entity missing `File` (schema:MediaObject) in its @type + MUST trigger a REQUIRED violation. + """ + do_entity_test( + __workflows_scripts_crates__.invalid_script_type, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Script: REQUIRED `File` type"], + expected_triggered_issues=["A Script MUST include `File` in its `@type`"], + ) + + +# --------------------------------------------------------------------------- +# Script name checks (MUST) +# --------------------------------------------------------------------------- + + +def test_valid_script_name(): + """ + A Script entity with a `name` property MUST pass REQUIRED validation. + """ + do_entity_test( + __workflows_scripts_crates__.valid_script_name, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_script_name(): + """ + A Script entity missing the `name` property MUST trigger a REQUIRED violation. + """ + do_entity_test( + __workflows_scripts_crates__.invalid_script_name, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Script or Workflow: REQUIRED `name`"], + expected_triggered_issues=["Scripts and Workflows MUST have a human-readable `name` property"], + ) + + +# --------------------------------------------------------------------------- +# Workflow type checks (MUST) +# --------------------------------------------------------------------------- + + +def test_valid_workflow_type(): + """ + A Workflow entity with `File`, `SoftwareSourceCode`, `ComputationalWorkflow` + in @type and all required properties MUST pass REQUIRED validation. + """ + do_entity_test( + __workflows_scripts_crates__.valid_workflow_type, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_workflow_type_missing_file(): + """ + A Workflow entity missing `File` (schema:MediaObject) in its @type + MUST trigger a REQUIRED violation. + """ + do_entity_test( + __workflows_scripts_crates__.invalid_workflow_type_missing_file, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Workflow: REQUIRED `File` type"], + expected_triggered_issues=["A Workflow MUST include `File` in its `@type`"], + ) + + +def test_invalid_workflow_type_missing_ssc(): + """ + A Workflow entity missing `SoftwareSourceCode` in its @type + MUST trigger a REQUIRED violation. + """ + do_entity_test( + __workflows_scripts_crates__.invalid_workflow_type_missing_ssc, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Workflow: REQUIRED `SoftwareSourceCode` type"], + expected_triggered_issues=["A Workflow MUST include `SoftwareSourceCode` in its `@type`"], + ) + + +# --------------------------------------------------------------------------- +# Workflow name checks (MUST) +# --------------------------------------------------------------------------- + + +def test_valid_workflow_name(): + """ + A Workflow entity with a `name` property MUST pass REQUIRED validation. + """ + do_entity_test( + __workflows_scripts_crates__.valid_workflow_name, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.3", + ) + + +def test_invalid_workflow_name(): + """ + A Workflow entity missing the `name` property MUST trigger a REQUIRED violation. + """ + do_entity_test( + __workflows_scripts_crates__.invalid_workflow_name, + models.Severity.REQUIRED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Script or Workflow: REQUIRED `name`"], + expected_triggered_issues=["Scripts and Workflows MUST have a human-readable `name` property"], + ) + + +# --------------------------------------------------------------------------- +# programmingLanguage checks (SHOULD) +# --------------------------------------------------------------------------- + + +def test_valid_programming_language(): + """ + A Script/Workflow with a `programmingLanguage` referencing a + `ComputerLanguage` entity SHOULD pass RECOMMENDED validation. + """ + do_entity_test( + __workflows_scripts_crates__.valid_programming_language, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=_GENERIC_RECOMMENDED_SKIP, + ) + + +def test_invalid_programming_language(): + """ + A Script/Workflow missing a `programmingLanguage` property + SHOULD trigger a RECOMMENDED warning. + """ + do_entity_test( + __workflows_scripts_crates__.invalid_programming_language, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Script or Workflow: RECOMMENDED `programmingLanguage`"], + expected_triggered_issues=["Scripts and Workflows SHOULD have a `programmingLanguage` property"], + ) + + +# --------------------------------------------------------------------------- +# Workflow conformsTo checks (SHOULD) +# --------------------------------------------------------------------------- + + +def test_valid_workflow_conformsTo(): + """ + A Workflow with `conformsTo` pointing to a versioned Bioschemas profile URI + SHOULD pass RECOMMENDED validation. + """ + do_entity_test( + __workflows_scripts_crates__.valid_workflow_conformsTo, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=_GENERIC_RECOMMENDED_SKIP, + ) + + +def test_invalid_workflow_conformsTo(): + """ + A Workflow missing `conformsTo` SHOULD trigger a RECOMMENDED warning. + """ + do_entity_test( + __workflows_scripts_crates__.invalid_workflow_conformsTo, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Workflow: RECOMMENDED Bioschemas `conformsTo`"], + expected_triggered_issues=[ + "Workflows SHOULD declare `conformsTo` referencing a versioned Bioschemas ComputationalWorkflow profile URI" + ], + ) + + +# --------------------------------------------------------------------------- +# Image encodingFormat checks (SHOULD) +# --------------------------------------------------------------------------- + + +def test_valid_image_encoding_format(): + """ + A Workflow image ImageObject with `encodingFormat` SHOULD pass + RECOMMENDED validation. + """ + do_entity_test( + __workflows_scripts_crates__.valid_image_encoding_format, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=_GENERIC_RECOMMENDED_SKIP, + ) + + +def test_invalid_image_encoding_format(): + """ + A Workflow image ImageObject missing `encodingFormat` SHOULD trigger + a RECOMMENDED warning. + """ + do_entity_test( + __workflows_scripts_crates__.invalid_image_encoding_format, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Script/Workflow ImageObject: RECOMMENDED `encodingFormat`"], + expected_triggered_issues=[ + "An ImageObject referenced via `image` from a Script or Workflow SHOULD have an `encodingFormat` property" + ], + ) + + +# --------------------------------------------------------------------------- +# Image about checks (SHOULD) +# --------------------------------------------------------------------------- + + +def test_valid_image_about(): + """ + A Workflow image ImageObject with `about` referencing the workflow + SHOULD pass RECOMMENDED validation. + """ + do_entity_test( + __workflows_scripts_crates__.valid_image_about, + models.Severity.RECOMMENDED, + True, + profile_identifier="ro-crate-1.3", + skip_checks=_GENERIC_RECOMMENDED_SKIP, + ) + + +def test_invalid_image_about(): + """ + A Workflow image ImageObject missing `about` SHOULD trigger a + RECOMMENDED warning. + """ + do_entity_test( + __workflows_scripts_crates__.invalid_image_about, + models.Severity.RECOMMENDED, + False, + profile_identifier="ro-crate-1.3", + expected_triggered_requirements=["Script/Workflow ImageObject: RECOMMENDED `about` reference"], + expected_triggered_issues=[ + "An ImageObject referenced via `image` from a Script or Workflow SHOULD have an `about` property " + "referencing the script or workflow" + ], + ) diff --git a/tests/integration/profiles/ro-crate/test_data_entity_metadata.py b/tests/integration/profiles/ro-crate/test_data_entity_metadata.py index 6317d32ee..6d2d2b412 100644 --- a/tests/integration/profiles/ro-crate/test_data_entity_metadata.py +++ b/tests/integration/profiles/ro-crate/test_data_entity_metadata.py @@ -18,8 +18,9 @@ import pytest from rocrate_validator import models, services +from rocrate_validator.utils.http import HttpRequester from tests.conftest import SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER -from tests.ro_crates import InvalidDataEntity, ValidROC +from tests.ro_crates import CRATES_DATA_PATH, InvalidDataEntity, ValidROC from tests.shared import do_entity_test # set up logging @@ -30,13 +31,65 @@ paths = InvalidDataEntity() +def test_explicit_jsonld_base_does_not_require_web_entities_in_payload(): + """Issue #240: the profile crate's relative IDs resolve against its HTTPS @base.""" + crate_path = CRATES_DATA_PATH / "profile-crate" + result = services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(crate_path), + profile_identifier="ro-crate-1.2", + requirement_severity=models.Severity.REQUIRED, + skip_availability_check=True, + ) + ) + + assert any(check.name == "Data Entity: REQUIRED resource availability" for check in result.executed_checks) + assert not [ + issue.message + for issue in result.issues + if "The RO-Crate does not include the Data Entity" in issue.message + and ("example1/" in issue.message or "index.html" in issue.message) + ] + + +def test_web_availability_uses_identifier_resolved_against_jsonld_base(monkeypatch): + """Web checks must request the expanded URI, not the relative @id.""" + requested_uris = [] + + class DownloadableResponse: + status_code = 200 + headers = {"Content-Type": "application/octet-stream"} + links = {} + + def raise_for_status(self): + pass + + def fake_head(uri, **_kwargs): + requested_uris.append(uri) + return DownloadableResponse() + + monkeypatch.setattr(HttpRequester(), "head", fake_head) + services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(CRATES_DATA_PATH / "profile-crate"), + profile_identifier="ro-crate-1.2", + requirement_severity=models.Severity.REQUIRED, + enforce_availability=True, + ) + ) + + expected_uri = "https://www.researchobject.org/workflow-run-crate/profiles/0.5/process_run_crate/index.html" + assert expected_uri in requested_uris + assert "index.html" not in requested_uris + + def test_missing_data_entity_reference(): """Test a RO-Crate without a root data entity.""" do_entity_test( paths.missing_hasPart_data_entity_reference, models.Severity.REQUIRED, False, - ["Data Entity: REQUIRED properties"], + ["Data Entity: REQUIRED Root linkage"], ["sort-and-change-case.ga", "foo/xxx"], ) diff --git a/tests/integration/profiles/ro-crate/test_file_descriptor_format.py b/tests/integration/profiles/ro-crate/test_file_descriptor_format.py index 5ee342bf8..4a4c42ded 100644 --- a/tests/integration/profiles/ro-crate/test_file_descriptor_format.py +++ b/tests/integration/profiles/ro-crate/test_file_descriptor_format.py @@ -12,9 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json import logging +import shutil +from pathlib import Path + +import pytest +from click.testing import CliRunner from rocrate_validator import models, services +from rocrate_validator.cli.main import cli +from rocrate_validator.utils.io_helpers.output.console import Console +from rocrate_validator.utils.io_helpers.output.text.formatters import ValidationResultTextOutputFormatter +from tests.conftest import SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER from tests.ro_crates import InvalidFileDescriptor, ValidROC from tests.shared import do_entity_test @@ -25,6 +35,157 @@ paths = InvalidFileDescriptor() +def _crate_with_suspicious_string_reference(tmp_path: Path, version: str) -> Path: + source = ( + Path(__file__).resolve().parents[3] + / "data" + / "crates" + / f"rocrate-{version}" + / "7_root_data_entity" + / "recommended_reference_type_for_licence" + / "valid" + ) + crate = tmp_path / f"rocrate-{version}" + shutil.copytree(source, crate) + metadata_path = crate / "ro-crate-metadata.json" + metadata = json.loads(metadata_path.read_text(encoding="utf-8")) + license_id = "https://creativecommons.org/licenses/by/4.0/" + root = next(entity for entity in metadata["@graph"] if entity.get("@id") == "./") + root["license"] = license_id + root["customReference"] = license_id + metadata["@context"] = [ + metadata["@context"], + {"customReference": "https://example.org/vocabulary/customReference"}, + ] + metadata_path.write_text(json.dumps(metadata, indent=2), encoding="utf-8") + return crate + + +@pytest.mark.parametrize("version", ["1.2", "1.3"]) +def test_extra_check_checks_are_opt_in_and_non_blocking(tmp_path: Path, version: str): + crate = _crate_with_suspicious_string_reference(tmp_path, version) + settings = models.ValidationSettings( + rocrate_uri=models.URI(crate), + profile_identifier=f"ro-crate-{version}", + requirement_severity=models.Severity.REQUIRED, + ) + + default_result = services.validate(settings) + assert default_result.passed() + assert default_result.extra_checks == [] + heuristic_check = next( + check + for check in default_result.statistics.extra_check_checks + if "suspicious entity references" in check.name.lower() + ) + assert heuristic_check.severity == models.Severity.REQUIRED + assert heuristic_check.extra_check + assert heuristic_check not in default_result.statistics.checks + conformance_check_count = default_result.statistics.total_checks + assert default_result.get_check_result(heuristic_check) is models.CheckResult.SKIPPED + + settings.extra_checks = True + extra_check_result = services.validate(settings) + assert extra_check_result.passed() + assert extra_check_result.statistics.total_checks == conformance_check_count + assert extra_check_result.statistics.executed_extra_check_checks == [heuristic_check] + assert len(extra_check_result.extra_checks) >= 2, ( + extra_check_result.validation_settings.extra_checks, + [ + (check.name, extra_check_result.get_check_result(check)) + for check in extra_check_result.statistics.checks + if "suspicious entity references" in check.name.lower() + ], + [detail.message for detail in extra_check_result.skipped_check_details], + ) + assert {extra_check.violating_property for extra_check in extra_check_result.extra_checks} >= { + "license", + "customReference", + } + assert extra_check_result.to_dict()["extra_checks"] + + console = Console(record=True, color_system=None, width=120) + console.print(ValidationResultTextOutputFormatter(extra_check_result)) + text_report = console.export_text() + assert "ExtraCheck Checks (1)" in text_report + assert "customReference" in text_report + + cli_result = CliRunner().invoke( + cli, + [ + "--no-interactive", + "validate", + str(crate), + "--profile-identifier", + f"ro-crate-{version}", + "--extra-checks", + "--output-format", + "json", + "--no-paging", + ], + ) + assert cli_result.exit_code == 0, cli_result.output + cli_report = json.loads(cli_result.output) + assert cli_report["passed"] + assert cli_report["extra_checks"] + + text_cli_result = CliRunner().invoke( + cli, + [ + "--no-interactive", + "validate", + str(crate), + "--profile-identifier", + f"ro-crate-{version}", + "--extra-checks", + "--no-paging", + ], + ) + assert text_cli_result.exit_code == 0, text_cli_result.output + assert "ExtraCheck Checks (1)" in text_cli_result.output + assert "customReference" in text_cli_result.output + + skipped_result = CliRunner().invoke( + cli, + [ + "--no-interactive", + "validate", + str(crate), + "--profile-identifier", + f"ro-crate-{version}", + "--extra-checks", + "--skip-checks", + heuristic_check.identifier, + "--output-format", + "json", + "--no-paging", + ], + ) + assert skipped_result.exit_code == 0, skipped_result.output + skipped_report = json.loads(skipped_result.output) + assert skipped_report["extra_checks"] == [] + + +@pytest.mark.parametrize("version", ["1.2", "1.3"]) +def test_required_about_reference_violation_is_detected_without_heuristic(tmp_path: Path, version: str): + crate = _crate_with_suspicious_string_reference(tmp_path, version) + metadata_path = crate / "ro-crate-metadata.json" + metadata = json.loads(metadata_path.read_text(encoding="utf-8")) + descriptor = next(entity for entity in metadata["@graph"] if entity.get("@id") == "ro-crate-metadata.json") + descriptor["about"] = {"@id": "./missing"} + metadata_path.write_text(json.dumps(metadata, indent=2), encoding="utf-8") + + result = services.validate( + models.ValidationSettings( + rocrate_uri=models.URI(crate), + profile_identifier=f"ro-crate-{version}", + requirement_severity=models.Severity.REQUIRED, + ) + ) + assert not result.passed() + assert result.extra_checks == [] + + def test_missing_file_descriptor(): """Test a RO-Crate without a file descriptor.""" rocrate_path = paths.missing_file_descriptor @@ -153,7 +314,7 @@ def test_invalid_jsonld_context(): False, ["File Descriptor JSON-LD format"], ["Unable to retrieve the JSON-LD context 'https://w3id.org/ro/terms/invalid/context'"], - profile_identifier="ro-crate", + profile_identifier="ro-crate-1.1", abort_on_first=True, ) @@ -195,3 +356,32 @@ def test_valid_jsonld_custom_term(): which contains custom terms. """ do_entity_test(ValidROC().rocrate_with_custom_terms, models.Severity.REQUIRED, True, [], []) + + +def test_singleton_entity_property_is_recommended_as_a_single_value(): + """ + Test the RO-Crate 1.1 recommendation that a singleton property array is + represented as a single value in compacted JSON-LD. + """ + do_entity_test( + ValidROC().workflow_roc, + models.Severity.RECOMMENDED, + False, + ["Entity properties compact representation"], + ['property "name" SHOULD be represented as a single value'], + profile_identifier="ro-crate-1.1", + skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], + rocrate_entity_patch={"sort-and-change-case.ga": {"name": ["sort-and-change-case"]}}, + ) + + +def test_singleton_entity_property_remains_valid_at_required_severity(): + """A singleton property array is valid when only REQUIRED checks are run.""" + do_entity_test( + ValidROC().workflow_roc, + models.Severity.REQUIRED, + True, + profile_identifier="ro-crate-1.1", + skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], + rocrate_entity_patch={"sort-and-change-case.ga": {"name": ["sort-and-change-case"]}}, + ) diff --git a/tests/integration/profiles/ro-crate/test_file_uri_data_entities.py b/tests/integration/profiles/ro-crate/test_file_uri_data_entities.py new file mode 100644 index 000000000..1e2b08180 --- /dev/null +++ b/tests/integration/profiles/ro-crate/test_file_uri_data_entities.py @@ -0,0 +1,98 @@ +"""Absolute file URIs identify resources independently of payload membership.""" + +import json +from pathlib import Path + +import pytest + +from rocrate_validator import models +from tests.shared import do_entity_test + + +@pytest.mark.parametrize("version", ["1.2", "1.3"]) +@pytest.mark.parametrize("form", ["file://", "file:", "file://localhost", "FILE://LOCALHOST"]) +@pytest.mark.parametrize("location", ["external-missing", "external-present", "payload"]) +def test_file_uri_payload_membership(tmp_path, version, form, location): + crate = tmp_path / "crate" + crate.mkdir() + # A sibling sharing the textual prefix must not count as payload. + external = tmp_path / "crate-external" + external.mkdir() + target = (crate if location == "payload" else external) / "a file.txt" + if location != "external-missing": + target.write_text("data") + entity_id = form + target.as_posix().replace(" ", "%20") + _validate(crate, version, entity_id, location != "payload") + + +@pytest.mark.parametrize("version", ["1.2", "1.3"]) +@pytest.mark.parametrize( + "entity_id", + [ + "file:///nonexistent/data.txt", + "file:/nonexistent/data.txt", + "file://localhost/nonexistent/data.txt", + "file://example.org/data.txt", + ], +) +def test_detached_file_uri(tmp_path, version, entity_id): + _validate(tmp_path, version, entity_id, True, detached=True) + + +@pytest.mark.parametrize("version", ["1.2", "1.3"]) +@pytest.mark.parametrize("entity_id", ["missing.txt", "/missing.txt", "file:///a file.txt"]) +def test_invalid_data_entity_references(tmp_path, version, entity_id): + _validate(tmp_path, version, entity_id, False) + + +@pytest.mark.parametrize("version", ["1.2", "1.3"]) +@pytest.mark.parametrize( + "entity_id, issue", + [ + ("file:relative.txt", "file URI path MUST be absolute"), + ("file://localhost", "file URI path MUST be absolute"), + ("file://example.org", "file URI path MUST be absolute"), + ("C:/data/x", "Windows drive path"), + ("//example.org/data.txt", "network-path reference"), + ], +) +@pytest.mark.parametrize("detached", [False, True]) +def test_identifier_path_syntax(tmp_path, version, entity_id, issue, detached): + _validate(tmp_path, version, entity_id, False, detached=detached, expected_issue=issue) + + +def _validate(crate, version, entity_id, valid, detached=False, expected_issue=None): + relative_path = "8_metadata_dataEntities/local_entity_reference/valid/ro-crate-metadata.json" + source = Path(f"tests/data/crates/rocrate-{version}") / relative_path + metadata = json.loads(source.read_text(encoding="utf-8")) + graph = metadata["@graph"] + root = next(e for e in graph if e["@id"] == "./") + files = [e for e in graph if e.get("@type") == "File"] + graph[:] = [e for e in graph if e not in files] + root["hasPart"] = [{"@id": entity_id}] + graph.append({"@id": entity_id, "@type": "File", "name": "Test file"}) + if detached: + root["@id"] = "https://example.org/crate/" + graph[0]["about"] = {"@id": root["@id"]} + descriptor = crate / "ro-crate-metadata.json" + descriptor.write_text(json.dumps(metadata)) + do_entity_test( + descriptor if detached else crate, + models.Severity.REQUIRED, + valid, + expected_triggered_issues=( + [expected_issue] + if expected_issue + else [] + if valid + else [ + "does not include the Data Entity" + if entity_id == "missing.txt" + else "invalid @id" + if " " in entity_id + else "MUST use a relative @id within the RO-Crate root" + ] + ), + profile_identifier=f"ro-crate-{version}", + skip_availability_check=True, + ) diff --git a/tests/integration/profiles/ro-crate/test_sparql_target_performance.py b/tests/integration/profiles/ro-crate/test_sparql_target_performance.py new file mode 100644 index 000000000..452b9a980 --- /dev/null +++ b/tests/integration/profiles/ro-crate/test_sparql_target_performance.py @@ -0,0 +1,158 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path +from time import perf_counter +from typing import TYPE_CHECKING, cast + +from rdflib import Graph, Namespace, URIRef +from rdflib.namespace import RDF, SH + +if TYPE_CHECKING: + from rdflib.query import ResultRow + +SCHEMA = Namespace("http://schema.org/") +DCT = Namespace("http://purl.org/dc/terms/") +RO_CRATE_1_1 = Namespace("https://github.com/crs4/rocrate-validator/profiles/ro-crate/") +RO_CRATE_1_2 = Namespace("https://github.com/crs4/rocrate-validator/profiles/ro-crate-1.2/") +PROFILE_1_1 = ( + Path(__file__).resolve().parents[4] + / "rocrate_validator" + / "profiles" + / "ro-crate" + / "1.1" + / "must" + / "4_data_entity_metadata.ttl" +) +PROFILE_1_2_SHOULD = ( + Path(__file__).resolve().parents[4] + / "rocrate_validator" + / "profiles" + / "ro-crate" + / "1.2" + / "should" + / "4_referenced_rocrate.ttl" +) + + +def _build_root_subquery_data_graph(entity_type, entity_count, about_count=249): + """Build a data graph with entities and a root subquery pattern.""" + data_graph = Graph() + root = URIRef("./") + expected = set() + for index in range(entity_count): + entity = URIRef(f"entity-{index}.dat") + expected.add(entity) + data_graph.add((entity, RDF.type, entity_type)) + data_graph.add((URIRef("ro-crate-metadata.json"), SCHEMA.about, root)) + for index in range(about_count): + about_entity = URIRef(f"about/entity-{index}") + data_graph.add((about_entity, SCHEMA.about, root)) + return data_graph, expected, root + + +def test_file_data_entity_target_scales_without_cross_product(): + """The production target must pre-compute roots before joining entities.""" + shapes_graph = Graph().parse(PROFILE_1_1, format="turtle") + target = shapes_graph.value(RO_CRATE_1_1.FileDataEntity, SH.target) + query = str(shapes_graph.value(target, SH.select)) + + # 500 entities + 250 about-entities + 1 descriptor = 501 triples. + # Without the root subquery, rdflib would build a cross-product of + # 500 x 250 = 125,000 intermediate rows before applying FILTERs. + # With the subquery, the root is pre-computed to a single binding, + # so the outer pattern processes only 500 combinations. + data_graph, expected, _ = _build_root_subquery_data_graph(SCHEMA.MediaObject, 500) + + started_at = perf_counter() + rows = data_graph.query(query, initNs={"schema": SCHEMA}) + actual = {cast("ResultRow", row).this for row in rows} + duration = perf_counter() - started_at + + assert actual == expected + assert duration < 3.0, f"FileDataEntity target took {duration:.2f}s; possible cross-product regression" + + +def test_directory_data_entity_target_scales_without_cross_product(): + """DirectoryDataEntity target must pre-compute roots before joining entities.""" + shapes_graph = Graph().parse(PROFILE_1_1, format="turtle") + target = shapes_graph.value(RO_CRATE_1_1.DirectoryDataEntity, SH.target) + query = str(shapes_graph.value(target, SH.select)) + + # Same structure as FileDataEntity: 500 Dataset entities + 250 about-entities. + # The root is excluded from results (FILTER ?this != ?root). + data_graph, expected, root = _build_root_subquery_data_graph(SCHEMA.Dataset, 500) + expected.discard(root) + + started_at = perf_counter() + rows = data_graph.query(query, initNs={"schema": SCHEMA}) + actual = {cast("ResultRow", row).this for row in rows} + duration = perf_counter() - started_at + + assert actual == expected + assert duration < 3.0, f"DirectoryDataEntity target took {duration:.2f}s; possible cross-product regression" + + +def test_referenced_rocrate_data_entity_target_scales_without_cross_product(): + """ReferencedROCrateDataEntityTarget must pre-compute roots before joining.""" + shapes_graph = Graph().parse(PROFILE_1_2_SHOULD, format="turtle") + query = str(shapes_graph.value(RO_CRATE_1_2.ReferencedROCrateDataEntityTarget, SH.select)) + + # 500 Dataset entities with dct:conformsTo + 250 about-entities + 1 descriptor. + # The target also filters by profile URI (STRSTARTS). + data_graph, expected, root = _build_root_subquery_data_graph(SCHEMA.Dataset, 500) + expected.discard(root) + profile_uri = URIRef("https://w3id.org/ro/crate/1.1") + for entity in expected: + data_graph.add((entity, DCT.conformsTo, profile_uri)) + + started_at = perf_counter() + rows = data_graph.query(query, initNs={"schema": SCHEMA, "dct": DCT}) + actual = {cast("ResultRow", row).this for row in rows} + duration = perf_counter() - started_at + + assert actual == expected + assert duration < 3.0, f"ReferencedROCrateDataEntity target took {duration:.2f}s; possible cross-product regression" + + +def test_referenced_rocrate_metadata_descriptor_target_scales_without_cross_product(): + """ReferencedROCrateMetadataDescriptorTarget must pre-compute roots before joining.""" + shapes_graph = Graph().parse(PROFILE_1_2_SHOULD, format="turtle") + query = str(shapes_graph.value(RO_CRATE_1_2.ReferencedROCrateMetadataDescriptorTarget, SH.select)) + + # 1 crate with 500 subjectOf descriptors + 1 root descriptor. + # Without the root subquery, the cross-product would be 500 x 1 = 500 + # (small here, but scales with multiple crates in real graphs). + data_graph = Graph() + root = URIRef("./") + expected = set() + crate = URIRef("referenced-crate/") + profile_uri = URIRef("https://w3id.org/ro/crate/1.1") + data_graph.add((crate, RDF.type, SCHEMA.Dataset)) + data_graph.add((crate, DCT.conformsTo, profile_uri)) + for index in range(500): + descriptor = URIRef(f"descriptor-{index}") + expected.add(descriptor) + data_graph.add((crate, SCHEMA.subjectOf, descriptor)) + data_graph.add((URIRef("ro-crate-metadata.json"), SCHEMA.about, root)) + + started_at = perf_counter() + rows = data_graph.query(query, initNs={"schema": SCHEMA, "dct": DCT}) + actual = {cast("ResultRow", row).this for row in rows} + duration = perf_counter() - started_at + + assert actual == expected + assert duration < 3.0, ( + f"ReferencedROCrateMetadataDescriptor target took {duration:.2f}s; possible cross-product regression" + ) diff --git a/tests/integration/profiles/test_metadata_only.py b/tests/integration/profiles/test_metadata_only.py index 21f46faca..1347a97cf 100644 --- a/tests/integration/profiles/test_metadata_only.py +++ b/tests/integration/profiles/test_metadata_only.py @@ -21,8 +21,8 @@ import pytest from rocrate_validator import models -from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER from tests.ro_crates import ValidROC +from tests.ro_crates_v1_3 import ContextualEntities, RootDataEntity, ValidROCrate13 from tests.shared import do_entity_test # set up logging @@ -50,7 +50,7 @@ def valid_roc_paths(): @pytest.mark.parametrize("valid_roc_path", valid_roc_paths()) -def test_valid_ro_crates_from_folder(valid_roc_path): +def test_valid_ro_crates_from_folder(valid_roc_path, profile_identifier="ro-crate-1.1"): """Test all valid RO-Crates.""" with tempfile.TemporaryDirectory() as tmpdirname: temp_path = Path(tmpdirname) / valid_roc_path.name @@ -64,14 +64,14 @@ def test_valid_ro_crates_from_folder(valid_roc_path): [], profile_identifier=( # ro-crate-1.1 can't be used for Five Safes crates as they conform to 1.2 - "five-safes-crate" if "five-safes-crate" in valid_roc_path.name else DEFAULT_PROFILE_IDENTIFIER + "five-safes-crate" if "five-safes-crate" in valid_roc_path.name else profile_identifier ), metadata_only=True, ) @pytest.mark.parametrize("valid_roc_path", valid_roc_paths()) -def test_valid_ro_crates_from_metadata_dict(valid_roc_path): +def test_valid_ro_crates_from_metadata_dict(valid_roc_path, profile_identifier="ro-crate-1.1"): """Test all valid RO-Crates using metadata dict.""" metadata_dict = None # Load the metadata dict from the RO-Crate @@ -88,8 +88,33 @@ def test_valid_ro_crates_from_metadata_dict(valid_roc_path): [], profile_identifier=( # ro-crate-1.1 can't be used for Five Safes crates as they conform to 1.2 - "five-safes-crate" if "five-safes-crate" in valid_roc_path.name else DEFAULT_PROFILE_IDENTIFIER + "five-safes-crate" if "five-safes-crate" in valid_roc_path.name else profile_identifier ), metadata_dict=metadata_dict, metadata_only=True, ) + + +def valid_roc_13_paths(): + """Existing RO-Crate 1.3 examples, separate from the original 1.1 fixtures.""" + valid_roc = ValidROCrate13() + contextual_entities = ContextualEntities() + return [ + pytest.param(valid_roc.attached, id="attached"), + pytest.param(valid_roc.attached_absolute_root, id="absolute-root"), + pytest.param(RootDataEntity().valid_additional_conformsTo_reference, id="profile-reference"), + pytest.param(contextual_entities.valid_software_application, id="software-application"), + pytest.param(contextual_entities.valid_computer_language, id="computer-language"), + ] + + +@pytest.mark.parametrize("valid_roc_path", valid_roc_13_paths()) +def test_valid_ro_crates_1_3_from_folder(valid_roc_path): + """Exercise the folder input mode with RO-Crate 1.3 fixtures.""" + test_valid_ro_crates_from_folder(valid_roc_path, profile_identifier="ro-crate-1.3") + + +@pytest.mark.parametrize("valid_roc_path", valid_roc_13_paths()) +def test_valid_ro_crates_1_3_from_metadata_dict(valid_roc_path): + """Exercise the dictionary input mode with RO-Crate 1.3 fixtures.""" + test_valid_ro_crates_from_metadata_dict(valid_roc_path, profile_identifier="ro-crate-1.3") diff --git a/tests/integration/profiles/workflow-ro-crate-1.1/test_computational_workflow_v1_1.py b/tests/integration/profiles/workflow-ro-crate-1.1/test_computational_workflow_v1_1.py new file mode 100644 index 000000000..7aa2f585b --- /dev/null +++ b/tests/integration/profiles/workflow-ro-crate-1.1/test_computational_workflow_v1_1.py @@ -0,0 +1,82 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator.models import Severity +from tests.ro_crates import WROCInvalidComputationalWorkflow_v1_1 +from tests.shared import do_entity_test + +# set up logging +logger = logging.getLogger(__name__) + + +def test_computational_workflow_no_input(): + """ + Test a Workflow RO-Crate where a computational workflow does not have + an input property. + """ + do_entity_test( + WROCInvalidComputationalWorkflow_v1_1().no_input, + Severity.OPTIONAL, + False, + ["Workflow RO-Crate ComputationalWorkflow MAY"], + ["A ComputationalWorkflow MAY have an input"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_computational_workflow_no_output(): + """ + Test a Workflow RO-Crate where a computational workflow does not have + an output property. + """ + do_entity_test( + WROCInvalidComputationalWorkflow_v1_1().no_output, + Severity.OPTIONAL, + False, + ["Workflow RO-Crate ComputationalWorkflow MAY"], + ["A ComputationalWorkflow MAY have an output"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_computational_workflow_bad_input(): + """ + Test a Workflow RO-Crate where a computational workflow has an input + property that does not point to a FormalParameter. + """ + do_entity_test( + WROCInvalidComputationalWorkflow_v1_1().bad_input, + Severity.REQUIRED, + False, + ["Workflow RO-Crate ComputationalWorkflow MUST"], + ["ComputationalWorkflow input and output MUST point to FormalParameter entities"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_computational_workflow_bad_output(): + """ + Test a Workflow RO-Crate where a computational workflow has an output + property that does not point to a FormalParameter. + """ + do_entity_test( + WROCInvalidComputationalWorkflow_v1_1().bad_output, + Severity.REQUIRED, + False, + ["Workflow RO-Crate ComputationalWorkflow MUST"], + ["ComputationalWorkflow input and output MUST point to FormalParameter entities"], + profile_identifier="workflow-ro-crate-1.1", + ) diff --git a/tests/integration/profiles/workflow-ro-crate-1.1/test_howtostep_v1_1.py b/tests/integration/profiles/workflow-ro-crate-1.1/test_howtostep_v1_1.py new file mode 100644 index 000000000..ae224f232 --- /dev/null +++ b/tests/integration/profiles/workflow-ro-crate-1.1/test_howtostep_v1_1.py @@ -0,0 +1,36 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator.models import Severity +from tests.ro_crates import WROCInvalidHowToStep_v1_1 +from tests.shared import do_entity_test + +# set up logging +logger = logging.getLogger(__name__) + + +def test_howtostep_no_position(): + """ + Test a Workflow RO-Crate where a HowToStep has no position. + """ + do_entity_test( + WROCInvalidHowToStep_v1_1().wroc_howtostep_no_position, + Severity.OPTIONAL, + False, + ["WROC HowToStep MAY"], + ["A HowToStep may indicate its position in the execution order via position"], + profile_identifier="workflow-ro-crate-1.1", + ) diff --git a/tests/integration/profiles/workflow-ro-crate-1.1/test_main_workflow_v1_1.py b/tests/integration/profiles/workflow-ro-crate-1.1/test_main_workflow_v1_1.py new file mode 100644 index 000000000..ff9775b7d --- /dev/null +++ b/tests/integration/profiles/workflow-ro-crate-1.1/test_main_workflow_v1_1.py @@ -0,0 +1,280 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +import pytest + +from rocrate_validator.models import Severity +from tests.ro_crates import InvalidMainWorkflow_v1_1, ValidWROC_v1_1 +from tests.shared import do_entity_test + +# set up logging +logger = logging.getLogger(__name__) + + +def test_main_workflow_bad_type(): + """ + Test a Workflow RO-Crate where the main workflow has an incorrect type. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_bad_type, + Severity.REQUIRED, + False, + ["Main Workflow definition"], + ["The Main Workflow types must include File, SoftwareSourceCode and ComputationalWorkflow"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_main_workflow_no_lang(): + """ + Test a Workflow RO-Crate where the main workflow does not have a + programmingLanguage property. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_no_lang, + Severity.REQUIRED, + False, + ["Main Workflow definition"], + ["The Main Workflow must refer to its language via programmingLanguage"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_main_workflow_no_image(): + """ + Test a Workflow RO-Crate where the main workflow does not have an + image property. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_no_image, + Severity.OPTIONAL, + False, + ["Main Workflow optional properties"], + ["The Crate MAY contain a Main Workflow Diagram; if present it MUST be referred to via 'image'"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_main_workflow_no_cwl_desc(): + """ + Test a Workflow RO-Crate where the main workflow does not have a + CWL description. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_no_cwl_desc, + Severity.OPTIONAL, + False, + ["Main Workflow optional properties"], + ["The Crate MAY contain a Main Workflow CWL Description; if present it MUST be referred to via 'subjectOf'"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_main_workflow_cwl_desc_bad_type(): + """ + Test a Workflow RO-Crate where the main workflow has a CWL description + but of the wrong type. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_cwl_desc_bad_type, + Severity.OPTIONAL, + False, + ["Main Workflow optional properties"], + ["The CWL Description type must be File, SoftwareSourceCode, HowTo"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_main_workflow_cwl_desc_no_lang(): + """ + Test a Workflow RO-Crate where the main workflow has a CWL description + but the description has no programmingLanguage. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_cwl_desc_no_lang, + Severity.OPTIONAL, + False, + ["Main Workflow optional properties"], + ["The CWL Description SHOULD have a language of https://w3id.org/workflowhub/workflow-ro-crate#cwl"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_main_workflow_file_existence(): + """ + Test a Workflow RO-Crate where the main workflow file is not in the crate. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_no_file, + Severity.REQUIRED, + False, + ["Main Workflow file existence"], + ["Main Workflow", "not found in crate"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_main_workflow_singleton_array(): + """A singleton-array mainEntity is JSON-LD-equivalent and passes REQUIRED checks.""" + do_entity_test( + ValidWROC_v1_1().workflow_roc_v1_1, + Severity.REQUIRED, + True, + profile_identifier="workflow-ro-crate-1.1", + metadata_only=True, + rocrate_entity_patch={"./": {"mainEntity": [{"@id": "sort-and-change-case.ga"}]}}, + ) + + +def test_main_workflow_singleton_array_should_be_unpacked(): + """RO-Crate 1.3 recommends unpacking a singleton array in compacted JSON-LD.""" + do_entity_test( + ValidWROC_v1_1().workflow_roc_v1_1, + Severity.RECOMMENDED, + False, + ["Entity properties compact representation"], + ['property "mainEntity" SHOULD be represented as a single value'], + profile_identifier="workflow-ro-crate-1.1", + metadata_only=True, + rocrate_entity_patch={"./": {"mainEntity": [{"@id": "sort-and-change-case.ga"}]}}, + ) + + +def test_main_workflow_singleton_array_file_existence(): + """A missing workflow in a singleton array produces the normal validation issue.""" + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_no_file, + Severity.REQUIRED, + False, + ["Main Workflow file existence"], + ["Main Workflow", "not found in crate"], + profile_identifier="workflow-ro-crate-1.1", + rocrate_entity_patch={"./": {"mainEntity": [{"@id": "sort-and-change-case.ga"}]}}, + ) + + +@pytest.mark.parametrize( + "main_entity", + [ + [], + [{"@id": "sort-and-change-case.ga"}, {"@id": "other-workflow.ga"}], + "sort-and-change-case.ga", + {"name": "sort-and-change-case.ga"}, + {"@id": 42}, + ], +) +def test_main_workflow_invalid_main_entity_is_reported(main_entity): + """Malformed mainEntity values fail validation without an unexpected exception.""" + do_entity_test( + ValidWROC_v1_1().workflow_roc_v1_1, + Severity.REQUIRED, + False, + ["Main Workflow file existence"], + ["mainEntity"], + profile_identifier="workflow-ro-crate-1.1", + metadata_only=True, + rocrate_entity_patch={"./": {"mainEntity": main_entity}}, + ) + + +def test_workflow_diagram_file_existence(): + """ + Test a Workflow RO-Crate where the workflow diagram file is not in the + crate. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_no_file, + Severity.OPTIONAL, + False, + ["Workflow-related files existence"], + ["Workflow diagram", "not found in crate"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_workflow_description_file_existence(): + """ + Test a Workflow RO-Crate where the workflow CWL description file is not in + the crate. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_no_file, + Severity.OPTIONAL, + False, + ["Workflow-related files existence"], + ["Workflow CWL description", "not found in crate"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_main_workflow_bad_conformsto(): + """ + Test a Workflow RO-Crate where the main workflow does not conform to the + bioschemas computational workflow 1.0 or later. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_bad_conformsto, + Severity.RECOMMENDED, + False, + ["Main Workflow recommended properties"], + ["The Main Workflow SHOULD comply with Bioschemas ComputationalWorkflow profile version 1.0 or later"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_main_workflow_no_step(): + """ + Test a Workflow RO-Crate where the main workflow does not have a + step property. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_no_step, + Severity.OPTIONAL, + False, + ["Main Workflow optional properties"], + ["The Main Workflow MAY have a step property"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_main_workflow_bad_step(): + """ + Test a Workflow RO-Crate where the main workflow has a step property + that does not point to HowToStep entities. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_bad_step, + Severity.REQUIRED, + False, + ["Main Workflow definition"], + ["Main Workflow step MUST point to HowToStep entities"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_main_workflow_with_steps_bad_type(): + """ + Test a Workflow RO-Crate that refers to steps but does not have the + HowTo type. + """ + do_entity_test( + InvalidMainWorkflow_v1_1().main_workflow_with_steps_bad_type, + Severity.REQUIRED, + False, + ["Main Workflow with steps MUST"], + ["A Main Workflow that links to steps MUST have the HowTo type"], + profile_identifier="workflow-ro-crate-1.1", + ) diff --git a/tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py b/tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py new file mode 100644 index 000000000..8b558edc7 --- /dev/null +++ b/tests/integration/profiles/workflow-ro-crate-1.1/test_valid_wroc_v1_1.py @@ -0,0 +1,38 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator.models import Severity +from tests.ro_crates import ValidWROC_v1_1 +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +def test_valid_workflow_roc_required(): + """Test Workflow RO-Crates that are valid at the REQUIRED level.""" + do_entity_test( + ValidWROC_v1_1().workflow_roc_v1_1, + Severity.REQUIRED, + True, + profile_identifier="workflow-ro-crate-1.1" + ) + do_entity_test( + ValidWROC_v1_1().workflow_roc_string_license_v1_1, + Severity.REQUIRED, + True, + profile_identifier="workflow-ro-crate-1.1", + metadata_only=True, + ) diff --git a/tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_crate_v1_1.py b/tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_crate_v1_1.py new file mode 100644 index 000000000..be6979ca0 --- /dev/null +++ b/tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_crate_v1_1.py @@ -0,0 +1,64 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator.models import Severity +from tests.ro_crates import WROCInvalidConformsTo_v1_1, WROCNoLicense_v1_1 +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +def test_wroc_rde_bad_conformsto(): + """ + Test a Workflow RO-Crate where the root data entity does not have a + conformsTo that points to the WROC 1.1 profile. + """ + do_entity_test( + WROCInvalidConformsTo_v1_1().wroc_bad_conforms_to, + Severity.RECOMMENDED, + False, + ["Root Data Entity Metadata"], + ["The Root Data Entity conformsTo SHOULD be an array that contains at least https://w3id.org/workflowhub/workflow-ro-crate/1.1"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_wroc_no_tests(): + """ + Test a Workflow RO-Crate with no test/ Dataset. + """ + do_entity_test( + WROCNoLicense_v1_1().wroc_no_license, + Severity.OPTIONAL, + False, + ["test directory"], + ["The test/ dir should be a Dataset"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_wroc_no_examples(): + """ + Test a Workflow RO-Crate with no examples/ Dataset. + """ + do_entity_test( + WROCNoLicense_v1_1().wroc_no_license, + Severity.OPTIONAL, + False, + ["examples directory"], + ["The examples/ dir should be a Dataset"], + profile_identifier="workflow-ro-crate-1.1", + ) diff --git a/tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_readme_v1_1.py b/tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_readme_v1_1.py new file mode 100644 index 000000000..cce6fe004 --- /dev/null +++ b/tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_readme_v1_1.py @@ -0,0 +1,49 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator.models import Severity +from tests.ro_crates import WROCInvalidReadme_v1_1 +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +def test_wroc_readme_not_about_crate(): + """ + Test a Workflow RO-Crate where the README.md is not about the crate. + """ + do_entity_test( + WROCInvalidReadme_v1_1().wroc_readme_not_about_crate, + Severity.RECOMMENDED, + False, + ["README.md properties"], + ["The README.md SHOULD be about the crate"], + profile_identifier="workflow-ro-crate-1.0", + ) + + +def test_wroc_readme_wrong_encoding_format(): + """ + Test a Workflow RO-Crate where the README.md has the wrong encodingFormat. + """ + do_entity_test( + WROCInvalidReadme_v1_1().wroc_readme_wrong_encoding_format, + Severity.RECOMMENDED, + False, + ["README.md properties"], + ["The README.md SHOULD have text/markdown as its encodingFormat"], + profile_identifier="workflow-ro-crate-1.0", + ) diff --git a/tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_root_metadata_v1_1.py b/tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_root_metadata_v1_1.py new file mode 100644 index 000000000..4eca0fefc --- /dev/null +++ b/tests/integration/profiles/workflow-ro-crate-1.1/test_wroc_root_metadata_v1_1.py @@ -0,0 +1,49 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rocrate_validator.models import Severity +from tests.ro_crates import WROCMainEntity_v1_1, WROCNoLicense_v1_1 +from tests.shared import do_entity_test + +logger = logging.getLogger(__name__) + + +def test_wroc_no_license(): + """ + Test a Workflow RO-Crate where the root data entity has no license. + """ + do_entity_test( + WROCNoLicense_v1_1().wroc_no_license, + Severity.REQUIRED, + False, + ["WROC Root Data Entity Required Properties"], + ["The Crate (Root Data Entity) must specify a license, which should be a URL but can also be a string"], + profile_identifier="workflow-ro-crate-1.1", + ) + + +def test_wroc_no_mainentity(): + """ + Test a Workflow RO-Crate where the root data entity has no mainEntity. + """ + do_entity_test( + WROCMainEntity_v1_1().wroc_no_mainentity, + Severity.REQUIRED, + False, + ["Main Workflow entity existence"], + ["The Main Workflow must be specified through a `mainEntity` property in the root data entity"], + profile_identifier="workflow-ro-crate-1.1", + ) diff --git a/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py b/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py index 5c98c1f59..91c8bb0f3 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py +++ b/tests/integration/profiles/workflow-ro-crate/test_main_workflow.py @@ -14,8 +14,11 @@ import logging +import pytest + from rocrate_validator.models import Severity -from tests.ro_crates import InvalidMainWorkflow +from tests.conftest import SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER +from tests.ro_crates import InvalidMainWorkflow, ValidROC from tests.shared import do_entity_test # set up logging @@ -32,7 +35,7 @@ def test_main_workflow_bad_type(): False, ["Main Workflow definition"], ["The Main Workflow must have types File, SoftwareSourceCode, ComputationalWorkflow"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -47,7 +50,7 @@ def test_main_workflow_no_lang(): False, ["Main Workflow definition"], ["The Main Workflow must refer to its language via programmingLanguage"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -62,7 +65,7 @@ def test_main_workflow_no_image(): False, ["Main Workflow optional properties"], ["The Crate MAY contain a Main Workflow Diagram; if present it MUST be referred to via 'image'"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -77,7 +80,7 @@ def test_main_workflow_no_cwl_desc(): False, ["Main Workflow optional properties"], ["The Crate MAY contain a Main Workflow CWL Description; if present it MUST be referred to via 'subjectOf'"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -92,7 +95,7 @@ def test_main_workflow_cwl_desc_bad_type(): False, ["Main Workflow optional properties"], ["The CWL Description type must be File, SoftwareSourceCode, HowTo"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -107,7 +110,7 @@ def test_main_workflow_cwl_desc_no_lang(): False, ["Main Workflow optional properties"], ["The CWL Description SHOULD have a language of https://w3id.org/workflowhub/workflow-ro-crate#cwl"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -121,7 +124,70 @@ def test_main_workflow_file_existence(): False, ["Main Workflow file existence"], ["Main Workflow", "not found in crate"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", + ) + + +def test_main_workflow_singleton_array(): + """A singleton-array mainEntity is JSON-LD-equivalent and passes REQUIRED checks.""" + do_entity_test( + ValidROC().workflow_roc, + Severity.REQUIRED, + True, + profile_identifier="workflow-ro-crate-1.0", + skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], + rocrate_entity_patch={"./": {"mainEntity": [{"@id": "sort-and-change-case.ga"}]}}, + ) + + +def test_main_workflow_singleton_array_should_be_unpacked(): + """RO-Crate 1.1 recommends unpacking a singleton array in compacted JSON-LD.""" + do_entity_test( + ValidROC().workflow_roc, + Severity.RECOMMENDED, + False, + ["Entity properties compact representation"], + ['property "mainEntity" SHOULD be represented as a single value'], + profile_identifier="workflow-ro-crate-1.0", + skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], + rocrate_entity_patch={"./": {"mainEntity": [{"@id": "sort-and-change-case.ga"}]}}, + ) + + +def test_main_workflow_singleton_array_file_existence(): + """A missing workflow in a singleton array produces the normal validation issue.""" + do_entity_test( + InvalidMainWorkflow().main_workflow_no_files, + Severity.REQUIRED, + False, + ["Main Workflow file existence"], + ["Main Workflow", "not found in crate"], + profile_identifier="workflow-ro-crate-1.0", + rocrate_entity_patch={"./": {"mainEntity": [{"@id": "sort-and-change-case.ga"}]}}, + ) + + +@pytest.mark.parametrize( + "main_entity", + [ + [], + [{"@id": "sort-and-change-case.ga"}, {"@id": "other-workflow.ga"}], + "sort-and-change-case.ga", + {"name": "sort-and-change-case.ga"}, + {"@id": 42}, + ], +) +def test_main_workflow_invalid_main_entity_is_reported(main_entity): + """Malformed mainEntity values fail validation without an unexpected exception.""" + do_entity_test( + ValidROC().workflow_roc, + Severity.REQUIRED, + False, + ["Main Workflow file existence"], + ["mainEntity"], + profile_identifier="workflow-ro-crate-1.0", + skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], + rocrate_entity_patch={"./": {"mainEntity": main_entity}}, ) @@ -136,7 +202,7 @@ def test_workflow_diagram_file_existence(): False, ["Workflow-related files existence"], ["Workflow diagram", "not found in crate"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -151,7 +217,7 @@ def test_workflow_description_file_existence(): False, ["Workflow-related files existence"], ["Workflow CWL description", "not found in crate"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -166,5 +232,5 @@ def test_main_workflow_bad_conformsto(): False, ["Main Workflow recommended properties"], ["The Main Workflow SHOULD comply with Bioschemas ComputationalWorkflow profile version 1.0 or later"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) diff --git a/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py b/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py index 256400230..8f9ddbce1 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py +++ b/tests/integration/profiles/workflow-ro-crate/test_valid_wroc.py @@ -28,13 +28,13 @@ def test_valid_workflow_roc_required(): ValidROC().workflow_roc, Severity.REQUIRED, True, - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], ) do_entity_test( ValidROC().workflow_roc_string_license, Severity.REQUIRED, True, - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], ) diff --git a/tests/integration/profiles/workflow-ro-crate/test_wroc_crate.py b/tests/integration/profiles/workflow-ro-crate/test_wroc_crate.py index cfc43460f..994af7e74 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_wroc_crate.py +++ b/tests/integration/profiles/workflow-ro-crate/test_wroc_crate.py @@ -31,7 +31,7 @@ def test_wroc_no_tests(): False, ["test directory"], ["The test/ dir should be a Dataset"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -45,5 +45,5 @@ def test_wroc_no_examples(): False, ["examples directory"], ["The examples/ dir should be a Dataset"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) diff --git a/tests/integration/profiles/workflow-ro-crate/test_wroc_descriptor.py b/tests/integration/profiles/workflow-ro-crate/test_wroc_descriptor.py index a3b633f17..74886a63e 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_wroc_descriptor.py +++ b/tests/integration/profiles/workflow-ro-crate/test_wroc_descriptor.py @@ -36,5 +36,5 @@ def test_wroc_descriptor_bad_conforms_to(): "The Metadata File Descriptor conformsTo SHOULD contain https://w3id.org/ro/crate/1.1 " "and https://w3id.org/workflowhub/workflow-ro-crate/1.0" ], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) diff --git a/tests/integration/profiles/workflow-ro-crate/test_wroc_readme.py b/tests/integration/profiles/workflow-ro-crate/test_wroc_readme.py index e314643b7..9a690b67e 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_wroc_readme.py +++ b/tests/integration/profiles/workflow-ro-crate/test_wroc_readme.py @@ -31,7 +31,7 @@ def test_wroc_readme_not_about_crate(): False, ["README.md properties"], ["The README.md SHOULD be about the crate"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -45,5 +45,5 @@ def test_wroc_readme_wrong_encoding_format(): False, ["README.md properties"], ["The README.md SHOULD have text/markdown as its encodingFormat"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) diff --git a/tests/integration/profiles/workflow-ro-crate/test_wroc_root_metadata.py b/tests/integration/profiles/workflow-ro-crate/test_wroc_root_metadata.py index 008db720f..1836f3f8c 100644 --- a/tests/integration/profiles/workflow-ro-crate/test_wroc_root_metadata.py +++ b/tests/integration/profiles/workflow-ro-crate/test_wroc_root_metadata.py @@ -31,7 +31,7 @@ def test_wroc_no_license(): False, ["WROC Root Data Entity Required Properties"], ["The Crate (Root Data Entity) must specify a license, which should be a URL but can also be a string"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) @@ -45,5 +45,5 @@ def test_wroc_no_mainentity(): False, ["Main Workflow entity existence"], ["The Main Workflow must be specified through a `mainEntity` property in the root data entity"], - profile_identifier="workflow-ro-crate", + profile_identifier="workflow-ro-crate-1.0", ) diff --git a/tests/integration/profiles/workflow-run-crate/test_wfrc_create_action.py b/tests/integration/profiles/workflow-run-crate/test_wfrc_create_action.py new file mode 100644 index 000000000..563523165 --- /dev/null +++ b/tests/integration/profiles/workflow-run-crate/test_wfrc_create_action.py @@ -0,0 +1,97 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from rocrate_validator.models import Severity +from tests.conftest import SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER +from tests.ro_crates import ValidROC +from tests.shared import do_entity_test + +EXPECTED_ISSUE = ( + "At least one CreateAction, ActivateAction or UpdateAction MUST reference the root data entity's mainEntity " + "as its instrument." +) + + +def test_wfrc_no_supported_action_with_main_entity_as_instrument(): + """Supported actions referencing another tool cannot satisfy the execution check.""" + do_entity_test( + ValidROC().workflow_run_crate, + Severity.REQUIRED, + False, + expected_triggered_issues=[EXPECTED_ISSUE], + profile_identifier="workflow-run-crate", + skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], + rocrate_entity_mod_sparql=""" + PREFIX schema: + DELETE { ?action schema:instrument ?workflow . } + INSERT { + ?action schema:instrument . + a schema:SoftwareApplication ; + schema:name "Another tool" . + } + WHERE { + ?root schema:mainEntity ?workflow . + ?metadata schema:about ?root . + FILTER( + STRENDS(STR(?metadata), "ro-crate-metadata.json") || + STRENDS(STR(?metadata), "ro-crate-metadata.jsonld") + ) + VALUES ?actionType { schema:CreateAction schema:ActivateAction schema:UpdateAction } + ?action a ?actionType ; + schema:instrument ?workflow . + } + """, + ) + + +@pytest.mark.parametrize("action_type", ["CreateAction", "ActivateAction", "UpdateAction"]) +def test_wfrc_no_action_with_main_entity_as_instrument(action_type): + """A valid instrument pointing to another tool cannot satisfy main entity execution.""" + do_entity_test( + ValidROC().workflow_run_crate, + Severity.REQUIRED, + False, + expected_triggered_issues=[EXPECTED_ISSUE], + profile_identifier="workflow-run-crate", + skip_checks=[SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER], + rocrate_entity_mod_sparql=f""" + PREFIX schema: + DELETE {{ ?action a ?originalType . }} + INSERT {{ ?action a schema:{action_type} . }} + WHERE {{ + VALUES ?originalType {{ schema:CreateAction schema:ActivateAction schema:UpdateAction }} + ?action a ?originalType . + }} ; + + PREFIX schema: + DELETE {{ ?action schema:instrument ?workflow . }} + INSERT {{ + ?action schema:instrument . + a schema:SoftwareApplication ; + schema:name "Another tool" . + }} + WHERE {{ + ?root schema:mainEntity ?workflow . + ?metadata schema:about ?root . + FILTER( + STRENDS(STR(?metadata), "ro-crate-metadata.json") || + STRENDS(STR(?metadata), "ro-crate-metadata.jsonld") + ) + ?action a schema:{action_type} ; + schema:instrument ?workflow . + }} + """, + ) diff --git a/tests/integration/test_compact_iri_validation.py b/tests/integration/test_compact_iri_validation.py new file mode 100644 index 000000000..1889353f7 --- /dev/null +++ b/tests/integration/test_compact_iri_validation.py @@ -0,0 +1,90 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Exercise compact IRIs through the public validator and profile overlays.""" + +import json +from copy import deepcopy +from unittest.mock import Mock + +import pytest + +from rocrate_validator import models, services +from rocrate_validator.utils.http import HttpRequester + +COMPACTION_CHECK = "Validation of the compaction format of the file descriptor" + + +@pytest.mark.parametrize("version", ["1.1", "1.2", "1.3"]) +@pytest.mark.parametrize("property_name", ["ex:property", "property", "unknown"]) +def test_compact_property_validation(version, property_name, tmp_path, monkeypatch): + context = { + "Dataset": "http://schema.org/Dataset", + "CreativeWork": "http://schema.org/CreativeWork", + "name": "http://schema.org/name", + "description": "http://schema.org/description", + "datePublished": "http://schema.org/datePublished", + "license": "http://schema.org/license", + "about": "http://schema.org/about", + "conformsTo": "http://purl.org/dc/terms/conformsTo", + } + response = Mock(status_code=200, headers={"Content-Type": "application/ld+json"}) + response.json.side_effect = lambda: {"@context": deepcopy(context)} + monkeypatch.setattr(HttpRequester(), "get", Mock(return_value=response)) + metadata = { + "@context": [ + f"https://w3id.org/ro/crate/{version}/context", + { + "ex": "https://example.org/", + "property": "https://example.org/property", + }, + ], + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": f"https://w3id.org/ro/crate/{version}"}, + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Prefix regression", + "description": "Test crate", + "datePublished": "2026-10-01", + "license": "https://creativecommons.org/publicdomain/zero/1.0/", + property_name: "value", + }, + ], + } + (tmp_path / "ro-crate-metadata.json").write_text(json.dumps(metadata)) + result = services.validate( + models.ValidationSettings( + rocrate_uri=str(tmp_path), + profile_identifier=f"ro-crate-{version}", + requirement_severity=models.Severity.REQUIRED, + ) + ) + checks = [check for check in result.executed_checks if check.name == COMPACTION_CHECK] + assert len(checks) == 1 + source_version = "1.2" if version == "1.3" else version + assert checks[0].requirement.profile.identifier == f"ro-crate-{source_version}" + accepted = property_name != "unknown" + assert result.get_executed_check_result(checks[0]) is accepted + issues = [issue for issue in result.get_issues() if issue.check.name == COMPACTION_CHECK] + assert len(issues) == (0 if accepted else 1) + if issues: + assert issues[0].profile_identifier == f"ro-crate-{version}" + assert issues[0].source_profile_identifier == f"ro-crate-{source_version}" + assert result.passed() is accepted, [issue.message for issue in result.get_issues()] diff --git a/tests/integration/test_local_crate_locations.py b/tests/integration/test_local_crate_locations.py new file mode 100644 index 000000000..7af6fdba8 --- /dev/null +++ b/tests/integration/test_local_crate_locations.py @@ -0,0 +1,132 @@ +"""Regression coverage for issue #238: filesystem names and file URI inputs.""" + +import json +import shutil +import zipfile + +import pytest +from click.testing import CliRunner +from rdflib import RDF, URIRef + +from rocrate_validator import models, services +from rocrate_validator.cli.main import cli +from rocrate_validator.rocrate import ROCrate +from rocrate_validator.rocrate.package_type import PackageType +from tests.ro_crates import ValidROCrate12 +from tests.ro_crates_v1_3 import ValidROCrate13 + + +@pytest.fixture +def minimal_crate(tmp_path): + crate = tmp_path / "my crate" + crate.mkdir() + metadata = { + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.1"}, + }, + { + "@id": "./", + "@type": "Dataset", + "name": "Minimal crate", + "description": "A minimal RO-Crate", + "datePublished": "2026-10-23", + "license": {"@id": "https://creativecommons.org/licenses/by/4.0/"}, + }, + ], + } + (crate / "ro-crate-metadata.json").write_text(json.dumps(metadata), encoding="utf-8") + return crate + + +@pytest.mark.parametrize("representation", ["path", "string", "uri", "relative"]) +@pytest.mark.parametrize("download", [False, True]) +def test_validate_crate_in_directory_with_spaces(minimal_crate, monkeypatch, representation, download): + monkeypatch.chdir(minimal_crate.parent) + inputs = { + "path": minimal_crate, + "string": str(minimal_crate), + "uri": minimal_crate.as_uri(), + "relative": minimal_crate.name, + } + result = services.validate( + models.ValidationSettings( + rocrate_uri=inputs[representation], + profile_identifier="ro-crate-1.1", + requirement_severity=models.Severity.REQUIRED, + disable_remote_crate_download=not download, + ) + ) + assert result.passed(), [issue.message for issue in result.get_issues()] + assert result.context.publicID == minimal_crate.as_uri() + "/" + graph = result.context.data_graph + descriptor = URIRef(minimal_crate.as_uri() + "/ro-crate-metadata.json") + root = URIRef(minimal_crate.as_uri() + "/") + assert (descriptor, RDF.type, URIRef("http://schema.org/CreativeWork")) in graph + assert (descriptor, URIRef("http://schema.org/about"), root) in graph + + +@pytest.mark.parametrize("as_uri", [False, True]) +def test_cli_validates_crate_with_spaces(minimal_crate, as_uri): + location = minimal_crate.as_uri() if as_uri else str(minimal_crate) + result = CliRunner().invoke( + cli, + [ + "validate", + "--profile-identifier", + "ro-crate-1.1", + "--requirement-severity", + "REQUIRED", + "--no-paging", + location, + ], + ) + assert result.exit_code == 0, result.output + + +@pytest.mark.parametrize("kind", ["zip", "metadata", "bagit"]) +def test_file_uri_loaders_preserve_packaging(minimal_crate, kind): + metadata = (minimal_crate / "ro-crate-metadata.json").read_text() + expected = PackageType.ATTACHED + if kind == "zip": + location = minimal_crate.parent / "my crate.zip" + with zipfile.ZipFile(location, "w") as archive: + archive.writestr("ro-crate-metadata.json", metadata) + elif kind == "metadata": + location = minimal_crate / "ro-crate-metadata.json" + expected = PackageType.DETACHED + else: + location = minimal_crate + (location / "bagit.txt").write_text("BagIt-Version: 1.0\nTag-File-Character-Encoding: UTF-8\n") + (location / "data").mkdir() + (location / "data" / "ro-crate-metadata.json").write_text(metadata) + (location / "ro-crate-metadata.json").unlink() + crate = ROCrate.new_instance(location.as_uri()) + assert crate.package_type is expected + assert crate.metadata.as_dict()["@graph"][1]["name"] == "Minimal crate" + + +@pytest.mark.parametrize("version,source", [("1.2", ValidROCrate12().attached), ("1.3", ValidROCrate13().attached)]) +def test_warm_validation_preserves_encoded_crate_bases(tmp_path, version, source): + first = tmp_path / "my crate" + second = tmp_path / "my%20crate" + shutil.copytree(source, first) + shutil.copytree(source, second) + validator = models.Validator( + models.ValidationSettings( + rocrate_uri=first, + profile_identifier=f"ro-crate-{version}", + requirement_severity=models.Severity.REQUIRED, + ) + ) + initial = validator.validate() + repeated = validator.validate(second.as_uri()) + assert initial.passed(), [issue.message for issue in initial.get_issues()] + assert repeated.passed(), [issue.message for issue in repeated.get_issues()] + assert initial.context.publicID == first.as_uri() + "/" + assert repeated.context.publicID == second.as_uri() + "/" + assert initial.context.prepared_validation_plan is repeated.context.prepared_validation_plan diff --git a/tests/integration/test_offline_mode.py b/tests/integration/test_offline_mode.py index 76039acac..7c7f6810d 100644 --- a/tests/integration/test_offline_mode.py +++ b/tests/integration/test_offline_mode.py @@ -301,7 +301,17 @@ def test_cli_cache_reset_yes_clears_entries(cli_runner, tmp_path, network_interc assert HttpRequester().cache_info()["entries"] == 0 -def test_cli_cache_warm_populates_profile_urls(cli_runner, tmp_path, network_interceptor): +@pytest.mark.parametrize( + ("profile_identifier", "context_url"), + [ + ("ro-crate-1.1", "https://w3id.org/ro/crate/1.1/context"), + ("ro-crate-1.2", "https://w3id.org/ro/crate/1.2/context"), + ("ro-crate-1.3", "https://w3id.org/ro/crate/1.3/context"), + ], +) +def test_cli_cache_warm_populates_profile_urls( + cli_runner, tmp_path, network_interceptor, profile_identifier, context_url +): cache_path = tmp_path / "cache" result = cli_runner.invoke( cli, @@ -312,7 +322,7 @@ def test_cli_cache_warm_populates_profile_urls(cli_runner, tmp_path, network_int "--cache-path", str(cache_path), "--profile-identifier", - "ro-crate-1.1", + profile_identifier, ], ) assert result.exit_code == 0, result.output @@ -322,7 +332,7 @@ def test_cli_cache_warm_populates_profile_urls(cli_runner, tmp_path, network_int # The URL must now be cached for offline use. HttpRequester.reset() HttpRequester.initialize_cache(cache_path=str(cache_path), cache_max_age=3600, offline=True) - assert HttpRequester().has_cached("https://w3id.org/ro/crate/1.1/context") is True + assert HttpRequester().has_cached(context_url) is True def test_cli_cache_warm_crate_caches_remote_archive(cli_runner, tmp_path, network_interceptor): diff --git a/tests/ro_crates.py b/tests/ro_crates.py index 4a450c9d0..3380b8ecd 100644 --- a/tests/ro_crates.py +++ b/tests/ro_crates.py @@ -134,6 +134,17 @@ def five_safes_crate_multiple_context(self) -> Path: return VALID_CRATES_DATA_PATH / "five-safes-crate-multiple-context" +class ValidWROC_v1_1: + + @property + def workflow_roc_v1_1(self) -> Path: + return VALID_CRATES_DATA_PATH / "workflow-roc-1.1" + + @property + def workflow_roc_string_license_v1_1(self) -> Path: + return VALID_CRATES_DATA_PATH / "workflow-roc-string-license-1.1" + + class InvalidFileDescriptor: base_path = INVALID_CRATES_DATA_PATH / "0_file_descriptor_format" @@ -362,6 +373,73 @@ def main_workflow_bad_conformsto(self) -> Path: return self.base_path / "main_workflow_bad_conformsto" +class InvalidMainWorkflow_v1_1: + base_path = INVALID_CRATES_DATA_PATH / "workflow-ro-crate-1.1" / "main_workflow" + + @property + def main_workflow_bad_type(self) -> Path: + return self.base_path / "main_workflow_bad_type" + + @property + def main_workflow_no_lang(self) -> Path: + return self.base_path / "main_workflow_no_lang" + + @property + def main_workflow_no_image(self) -> Path: + return self.base_path / "main_workflow_no_image" + + @property + def main_workflow_no_cwl_desc(self) -> Path: + return self.base_path / "main_workflow_no_cwl_desc" + + @property + def main_workflow_cwl_desc_bad_type(self) -> Path: + return self.base_path / "main_workflow_cwl_desc_bad_type" + + @property + def main_workflow_cwl_desc_no_lang(self) -> Path: + return self.base_path / "main_workflow_cwl_desc_no_lang" + + @property + def main_workflow_no_file(self) -> Path: + return self.base_path / "main_workflow_no_file" + + @property + def main_workflow_bad_conformsto(self) -> Path: + return self.base_path / "main_workflow_bad_conformsto" + + @property + def main_workflow_no_step(self) -> Path: + return self.base_path / "main_workflow_no_step" + + @property + def main_workflow_bad_step(self) -> Path: + return self.base_path / "main_workflow_bad_step" + + @property + def main_workflow_with_steps_bad_type(self) -> Path: + return self.base_path / "main_workflow_with_steps_bad_type" + + +class WROCInvalidComputationalWorkflow_v1_1: + base_path = INVALID_CRATES_DATA_PATH / "workflow-ro-crate-1.1" / "computational_workflow" + @property + def no_input(self) -> Path: + return self.base_path / "no_input" + + @property + def no_output(self) -> Path: + return self.base_path / "no_output" + + @property + def bad_input(self) -> Path: + return self.base_path / "bad_input" + + @property + def bad_output(self) -> Path: + return self.base_path / "bad_output" + + class WROCInvalidConformsTo: base_path = INVALID_CRATES_DATA_PATH / "2_wroc_descriptor" @@ -370,6 +448,14 @@ def wroc_descriptor_bad_conforms_to(self) -> Path: return self.base_path / "wroc_descriptor_bad_conforms_to" +class WROCInvalidConformsTo_v1_1: + base_path = INVALID_CRATES_DATA_PATH / "workflow-ro-crate-1.1" / "wroc_crate" + + @property + def wroc_bad_conforms_to(self) -> Path: + return self.base_path / "bad_profile" + + class WROCInvalidReadme: base_path = INVALID_CRATES_DATA_PATH / "1_wroc_crate/" @@ -382,6 +468,18 @@ def wroc_readme_wrong_encoding_format(self) -> Path: return self.base_path / "readme_wrong_encoding_format" +class WROCInvalidReadme_v1_1: + base_path = INVALID_CRATES_DATA_PATH / "workflow-ro-crate-1.1" / "wroc_crate/" + + @property + def wroc_readme_not_about_crate(self) -> Path: + return self.base_path / "readme_not_about_crate" + + @property + def wroc_readme_wrong_encoding_format(self) -> Path: + return self.base_path / "readme_wrong_encoding_format" + + class WROCNoLicense: base_path = INVALID_CRATES_DATA_PATH / "1_wroc_crate/" @@ -390,6 +488,14 @@ def wroc_no_license(self) -> Path: return self.base_path / "no_license" +class WROCNoLicense_v1_1: + base_path = INVALID_CRATES_DATA_PATH / "workflow-ro-crate-1.1" / "wroc_crate/" + + @property + def wroc_no_license(self) -> Path: + return self.base_path / "no_license" + + class WROCMainEntity: base_path = INVALID_CRATES_DATA_PATH / "1_wroc_crate/" @@ -398,6 +504,22 @@ def wroc_no_mainentity(self) -> Path: return self.base_path / "no_mainentity" +class WROCMainEntity_v1_1: + base_path = INVALID_CRATES_DATA_PATH / "workflow-ro-crate-1.1" / "wroc_crate/" + + @property + def wroc_no_mainentity(self) -> Path: + return self.base_path / "no_mainentity" + + +class WROCInvalidHowToStep_v1_1: + base_path = INVALID_CRATES_DATA_PATH / "workflow-ro-crate-1.1" / "howtostep" + + @property + def wroc_howtostep_no_position(self) -> Path: + return self.base_path / "howtostep_no_position" + + class InvalidProcRC: base_path = INVALID_CRATES_DATA_PATH / "3_process_run_crate/" diff --git a/tests/ro_crates_v1_2.py b/tests/ro_crates_v1_2.py index 295f22297..60ad396b9 100644 --- a/tests/ro_crates_v1_2.py +++ b/tests/ro_crates_v1_2.py @@ -82,6 +82,14 @@ def valid_named_entity_id_format(self) -> Path: def invalid_named_entity_id_format(self) -> Path: return self.METADATA_DOCUMENT_CRATES_PATH / "named_entity_id_format" / "invalid" + @property + def valid_schema_org_iri_protocol(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "schema_org_iri_protocol" / "valid" + + @property + def invalid_schema_org_iri_protocol(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "schema_org_iri_protocol" / "invalid" + class MetadataDocumentFormat: METADATA_DOCUMENT_FORMAT_CRATES_PATH = MetadataDocument.METADATA_DOCUMENT_CRATES_PATH / "format" @@ -142,15 +150,33 @@ def __remote_base_url__(self) -> str: @property def valid_local_descriptor_filename(self) -> Path: - return self.DETACHED_ROCRATES_CRATES_PATH / "naming-convention" / "local-descriptor" / "valid" + return ( + self.DETACHED_ROCRATES_CRATES_PATH + / "naming-convention" + / "local-descriptor" + / "valid" + / "basic-ro-crate-metadata.json" + ) @property def invalid_local_descriptor_filename(self) -> Path: - return self.DETACHED_ROCRATES_CRATES_PATH / "naming-convention" / "local-descriptor" / "invalid" + return ( + self.DETACHED_ROCRATES_CRATES_PATH + / "naming-convention" + / "local-descriptor" + / "invalid" + / "ro-crate-metadata.json" + ) @property def valid_root_data_entity_identifier_when_online_available(self) -> Path: - return self.DETACHED_ROCRATES_CRATES_PATH / "root-data-entity-identifier" / "online-available" / "valid" + return ( + self.DETACHED_ROCRATES_CRATES_PATH + / "root-data-entity-identifier" + / "online-available" + / "valid" + / "prefix-ro-crate-metadata.json" + ) @property def invalid_root_data_entity_identifier_when_online_available(self) -> Path: @@ -184,6 +210,14 @@ def valid_recommended_name(self) -> Path: def invalid_recommended_name(self) -> Path: return self.METADATA_ENTITIES_CRATES_PATH / "recommended_name" / "invalid" + @property + def valid_preprocessing_candidates(self) -> Path: + return self.METADATA_ENTITIES_CRATES_PATH / "preprocessing_candidates" / "valid" + + @property + def invalid_preprocessing_candidates(self) -> Path: + return self.METADATA_ENTITIES_CRATES_PATH / "preprocessing_candidates" / "invalid" + class MetadataDescriptor: METADATA_DESCRIPTOR_CRATES_PATH = BASE_PATH / "6_metadata_descriptor" @@ -226,11 +260,21 @@ def invalid_required_downloadable_citeAs(self) -> Path: @property def valid_recommended_citeAs_for_resolvable_id(self) -> Path: - return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_citeas_for_resolvable_id" / "valid" + return ( + self.ROOT_DATA_ENTITY_CRATES_PATH + / "recommended_citeas_for_resolvable_id" + / "valid" + / "citeas-ro-crate-metadata.json" + ) @property def invalid_recommended_citeAs_for_resolvable_id(self) -> Path: - return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_citeas_for_resolvable_id" / "invalid" + return ( + self.ROOT_DATA_ENTITY_CRATES_PATH + / "recommended_citeas_for_resolvable_id" + / "invalid" + / "citeas-ro-crate-metadata.json" + ) @property def valid_additional_conformsTo_reference(self) -> Path: @@ -399,11 +443,18 @@ def invalid_local_entity_reference(self) -> Path: @property def valid_detached_rocrate_dataEntities(self) -> Path: - return self.DATA_ENTITIES_CRATES_PATH / "detached_rocrate_dataEntities" / "valid" + return ( + self.DATA_ENTITIES_CRATES_PATH / "detached_rocrate_dataEntities" / "valid" / "basic-ro-crate-metadata.json" + ) @property def invalid_detached_rocrate_dataEntities(self) -> Path: - return self.DATA_ENTITIES_CRATES_PATH / "detached_rocrate_dataEntities" / "invalid" + return ( + self.DATA_ENTITIES_CRATES_PATH + / "detached_rocrate_dataEntities" + / "invalid" + / "basic-ro-crate-metadata.json" + ) @property def valid_recommended_properties(self) -> Path: @@ -767,11 +818,11 @@ def attached_absolute_root(self) -> Path: @property def detached(self) -> Path: - return self.base_path / "detached" / "dataset-ro-crate-metadata.json" + return self.base_path / "detached-1.2" / "dataset-ro-crate-metadata.json" @property def detached_prefixed(self) -> Path: - return self.base_path / "detached" / "test-ro-crate-metadata.json" + return self.base_path / "detached-1.2" / "test-ro-crate-metadata.json" class InvalidROCrate12: diff --git a/tests/ro_crates_v1_3.py b/tests/ro_crates_v1_3.py new file mode 100644 index 000000000..53744e2f6 --- /dev/null +++ b/tests/ro_crates_v1_3.py @@ -0,0 +1,839 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +from pytest import fixture + +CURRENT_PATH = Path(__file__).resolve().parent +TEST_DATA_PATH = (CURRENT_PATH / "data").absolute() +CRATES_DATA_PATH = TEST_DATA_PATH / "crates" +VALID_CRATES_DATA_PATH = CRATES_DATA_PATH / "valid" +INVALID_CRATES_DATA_PATH = CRATES_DATA_PATH / "invalid" + + +@fixture +def ro_crates_path() -> Path: + return CRATES_DATA_PATH + + +BASE_PATH = CRATES_DATA_PATH / "rocrate-1.3" + + +class MetadataDocument: + METADATA_DOCUMENT_CRATES_PATH = BASE_PATH / "1_metadata_document" + + @property + def invalid_context_reference(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "context_reference" / "invalid" + + @property + def valid_context_reference(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "context_reference" / "valid" + + @property + def not_referenced_contextual_entity(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "referenced_contextual_entities" / "invalid" + + @property + def valid_referenced_contextual_entity(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "referenced_contextual_entities" / "valid" + + @property + def described_contextual_entity(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "described_contextual_entities" / "valid" + + @property + def not_described_contextual_entity(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "described_contextual_entities" / "invalid" + + @property + def valid_no_parent_traversal(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "no_parent_traversal" / "valid" + + @property + def invalid_no_parent_traversal(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "no_parent_traversal" / "invalid" + + @property + def valid_utf8_identifiers(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "utf8_identifiers" / "valid" + + @property + def invalid_utf8_identifiers(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "utf8_identifiers" / "invalid" + + @property + def valid_named_entity_id_format(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "named_entity_id_format" / "valid" + + @property + def invalid_named_entity_id_format(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "named_entity_id_format" / "invalid" + + @property + def valid_schema_org_iri_protocol(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "schema_org_iri_protocol" / "valid" + + @property + def invalid_schema_org_iri_protocol(self) -> Path: + return self.METADATA_DOCUMENT_CRATES_PATH / "schema_org_iri_protocol" / "invalid" + + +class MetadataDocumentFormat: + METADATA_DOCUMENT_FORMAT_CRATES_PATH = MetadataDocument.METADATA_DOCUMENT_CRATES_PATH / "format" + + @property + def not_compacted(self) -> Path: + return self.METADATA_DOCUMENT_FORMAT_CRATES_PATH / "compacted" + + @property + def not_flattened(self) -> Path: + return self.METADATA_DOCUMENT_FORMAT_CRATES_PATH / "flattened" + + @property + def not_jsonld(self) -> Path: + return self.METADATA_DOCUMENT_FORMAT_CRATES_PATH / "jsonld" + + @property + def not_utf8(self) -> Path: + return self.METADATA_DOCUMENT_FORMAT_CRATES_PATH / "utf8" + + +class AttachedROCrates: + ATTACHED_ROCRATES_CRATES_PATH = BASE_PATH / "2_attached_rocrates" + + @property + def valid_preview_not_in_hasPart(self) -> Path: + return self.ATTACHED_ROCRATES_CRATES_PATH / "attached-preview-not-in-hasPart" / "valid" + + @property + def invalid_preview_not_in_hasPart(self) -> Path: + return self.ATTACHED_ROCRATES_CRATES_PATH / "attached-preview-not-in-hasPart" / "invalid" + + @property + def valid_non_relative_root_entity_id(self) -> Path: + return self.ATTACHED_ROCRATES_CRATES_PATH / "non-relative-root-identifier" / "valid" + + @property + def invalid_non_relative_root_entity_id(self) -> Path: + return self.ATTACHED_ROCRATES_CRATES_PATH / "non-relative-root-identifier" / "invalid" + + @property + def valid_relative_root_entity_id(self) -> Path: + return self.ATTACHED_ROCRATES_CRATES_PATH / "relative-root-identifier" / "valid" + + @property + def invalid_relative_root_entity_id(self) -> Path: + return self.ATTACHED_ROCRATES_CRATES_PATH / "relative-root-identifier" / "invalid" + + +class DetachedROCrates: + DETACHED_ROCRATES_CRATES_PATH = BASE_PATH / "3_detached_rocrates" + + __remote_sha__ = "8a3efd8b2b6a87f169ec6f69e893b3cf59538479" + + @property + def __remote_base_url__(self) -> str: + return f"https://bitbucket.org/simleo/ro-crates/raw/{self.__remote_sha__}" + + @property + def valid_local_descriptor_filename(self) -> Path: + return ( + self.DETACHED_ROCRATES_CRATES_PATH + / "naming-convention" + / "local-descriptor" + / "valid" + / "basic-ro-crate-metadata.json" + ) + + @property + def invalid_local_descriptor_filename(self) -> Path: + return ( + self.DETACHED_ROCRATES_CRATES_PATH + / "naming-convention" + / "local-descriptor" + / "invalid" + / "ro-crate-metadata.json" + ) + + @property + def valid_root_data_entity_identifier_when_online_available(self) -> Path: + return ( + self.DETACHED_ROCRATES_CRATES_PATH + / "root-data-entity-identifier" + / "online-available" + / "valid" + / "prefix-ro-crate-metadata.json" + ) + + @property + def invalid_root_data_entity_identifier_when_online_available(self) -> Path: + return f"{self.__remote_base_url__}/online-available/invalid/basic-ro-crate-metadata.json" + + @property + def valid_web_data_entity(self) -> Path: + return f"{self.__remote_base_url__}/online-available/valid/basic-ro-crate-metadata.json" + + @property + def invalid_web_data_entity(self) -> Path: + return f"{self.__remote_base_url__}/online-available/invalid/basic-ro-crate-metadata.json" + + +class MetadataEntities: + METADATA_ENTITIES_CRATES_PATH = BASE_PATH / "5_metadata_entities" + + @property + def valid_recommended_schema_type(self) -> Path: + return self.METADATA_ENTITIES_CRATES_PATH / "recommended_schema_type" / "valid" + + @property + def invalid_recommended_schema_type(self) -> Path: + return self.METADATA_ENTITIES_CRATES_PATH / "recommended_schema_type" / "invalid" + + @property + def valid_recommended_name(self) -> Path: + return self.METADATA_ENTITIES_CRATES_PATH / "recommended_name" / "valid" + + @property + def invalid_recommended_name(self) -> Path: + return self.METADATA_ENTITIES_CRATES_PATH / "recommended_name" / "invalid" + + +class MetadataDescriptor: + METADATA_DESCRIPTOR_CRATES_PATH = BASE_PATH / "6_metadata_descriptor" + + @property + def valid_single_value_conformsTo(self) -> Path: + return self.METADATA_DESCRIPTOR_CRATES_PATH / "recommended_conformsTo" / "single_value" / "valid" + + @property + def invalid_single_value_conformsTo(self) -> Path: + return self.METADATA_DESCRIPTOR_CRATES_PATH / "recommended_conformsTo" / "single_value" / "invalid" + + @property + def valid_recommended_prefix_conformsTo(self) -> Path: + return self.METADATA_DESCRIPTOR_CRATES_PATH / "recommended_conformsTo" / "recommended_prefix" / "valid" + + @property + def invalid_recommended_prefix_conformsTo(self) -> Path: + return self.METADATA_DESCRIPTOR_CRATES_PATH / "recommended_conformsTo" / "recommended_prefix" / "invalid" + + +class RootDataEntity: + ROOT_DATA_ENTITY_CRATES_PATH = BASE_PATH / "7_root_data_entity" + + @property + def valid_required_datePublished(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "required_datePublished" / "valid" + + @property + def invalid_required_datePublished(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "required_datePublished" / "invalid" + + @property + def valid_required_downloadable_citeAs(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "required_downloadable_citeas" / "valid" + + @property + def invalid_required_downloadable_citeAs(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "required_downloadable_citeas" / "invalid" + + @property + def valid_recommended_citeAs_for_resolvable_id(self) -> Path: + return ( + self.ROOT_DATA_ENTITY_CRATES_PATH + / "recommended_citeas_for_resolvable_id" + / "valid" + / "citeas-ro-crate-metadata.json" + ) + + @property + def invalid_recommended_citeAs_for_resolvable_id(self) -> Path: + return ( + self.ROOT_DATA_ENTITY_CRATES_PATH + / "recommended_citeas_for_resolvable_id" + / "invalid" + / "citeas-ro-crate-metadata.json" + ) + + @property + def valid_additional_conformsTo_reference(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "additional_conformsTo" / "valid" + + @property + def invalid_additional_conformsTo_reference(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "additional_conformsTo" / "invalid" + + @property + def missing_root(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "missing_root" / "invalid" + + @property + def invalid_root_type(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "invalid_root_type" / "invalid" + + @property + def invalid_root_value(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "invalid_root_value" / "invalid" + + @property + def recommended_root_value(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_root_value" / "valid" + + @property + def invalid_root_date(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "invalid_root_date" / "invalid" + + @property + def invalid_recommended_root_date(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "invalid_recommended_root_date" / "invalid" + + @property + def missing_root_name(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "missing_root_name" / "invalid" + + @property + def missing_root_description(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "missing_root_description" / "invalid" + + @property + def missing_root_license(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "missing_root_license" / "invalid" + + @property + def missing_root_license_name(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "missing_root_license_name" / "invalid" + + @property + def missing_root_license_description(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "missing_root_license_description" / "invalid" + + @property + def valid_referenced_generic_data_entities(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "valid_referenced_generic_data_entities" / "valid" + + @property + def valid_recommended_identifier_resolution(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_identifier_resolution" / "valid" + + @property + def invalid_recommended_identifier_resolution(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_identifier_resolution" / "invalid" + + @property + def valid_recommended_publisher(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_publisher" / "valid" + + @property + def invalid_recommended_publisher(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_publisher" / "invalid" + + @property + def valid_recommended_funding(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_funding" / "valid" + + @property + def invalid_recommended_funding_no_funder(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_funding" / "invalid_no_funder" + + @property + def invalid_recommended_funding_non_org_funder(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_funding" / "invalid_non_org_funder" + + @property + def invalid_recommended_funding_no_project_funder(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_funding" / "invalid_no_project_funder" + + # R1: datePublished day precision (SHOULD) + @property + def valid_recommended_datePublished_day_precision(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_datepublished_day_precision" / "valid" + + @property + def invalid_recommended_datePublished_day_precision(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_datepublished_day_precision" / "invalid" + + # R2: hasPart MUST reference all Data Entities + @property + def valid_required_hasPart_all_data_entities(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "required_haspart_all_data_entities" / "valid" + + @property + def profile_root_contextual_datasets(self) -> Path: + return ( + self.ROOT_DATA_ENTITY_CRATES_PATH + / "required_haspart_all_data_entities" + / "profile_root_contextual_datasets" + ) + + @property + def invalid_required_hasPart_all_data_entities(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "required_haspart_all_data_entities" / "invalid" + + @property + def invalid_hasPart_workflow_not_in_haspart(self) -> Path: + return ( + self.ROOT_DATA_ENTITY_CRATES_PATH / "required_haspart_all_data_entities" / "invalid_workflow_not_in_haspart" + ) + + @property + def invalid_hasPart_web_entity_not_in_haspart(self) -> Path: + return ( + self.ROOT_DATA_ENTITY_CRATES_PATH + / "required_haspart_all_data_entities" + / "invalid_web_entity_not_in_haspart" + ) + + @property + def invalid_hasPart_dataset_not_in_haspart(self) -> Path: + return ( + self.ROOT_DATA_ENTITY_CRATES_PATH / "required_haspart_all_data_entities" / "invalid_dataset_not_in_haspart" + ) + + # R3: identifier SHOULD be present if PID exists (SHOULD) + @property + def valid_recommended_identifier_if_pid(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_identifier_if_pid" / "valid" + + @property + def invalid_recommended_identifier_if_pid(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_identifier_if_pid" / "invalid" + + # R4: identifier SHOULD use PropertyValue approach (SHOULD) + @property + def valid_recommended_identifier_propertyvalue(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_identifier_propertyvalue" / "valid" + + @property + def invalid_recommended_identifier_propertyvalue(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_identifier_propertyvalue" / "invalid" + + # R5: conformsTo SHOULD be present if profiles exist (SHOULD) + @property + def valid_recommended_conformsto_if_profiles(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_conformsto_if_profiles" / "valid" + + @property + def invalid_recommended_conformsto_if_profiles(self) -> Path: + return self.ROOT_DATA_ENTITY_CRATES_PATH / "recommended_conformsto_if_profiles" / "invalid" + + +class DataEntities: + DATA_ENTITIES_CRATES_PATH = BASE_PATH / "8_metadata_dataEntities" + + @property + def valid_local_entity_reference(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "local_entity_reference" / "valid" + + @property + def invalid_local_entity_reference(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "local_entity_reference" / "invalid" + + @property + def valid_detached_rocrate_dataEntities(self) -> Path: + return ( + self.DATA_ENTITIES_CRATES_PATH / "detached_rocrate_dataEntities" / "valid" / "basic-ro-crate-metadata.json" + ) + + @property + def invalid_detached_rocrate_dataEntities(self) -> Path: + return ( + self.DATA_ENTITIES_CRATES_PATH + / "detached_rocrate_dataEntities" + / "invalid" + / "basic-ro-crate-metadata.json" + ) + + @property + def valid_recommended_properties(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "recommended_properties" / "valid" + + @property + def invalid_recommended_properties(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "recommended_properties" / "invalid" + + @property + def valid_recommended_encoding_format(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "recommended_encoding_format" / "valid" + + @property + def invalid_recommended_encoding_format(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "recommended_encoding_format" / "invalid" + + @property + def valid_web_entity_downloadable(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "web_entity_downloadable" / "valid" + + @property + def invalid_web_entity_splash_page(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "web_entity_downloadable" / "invalid" + + @property + def valid_recommended_content_url(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "web_entity_content_url" / "valid" + + @property + def invalid_recommended_content_url(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "web_entity_content_url" / "invalid" + + @property + def valid_recommended_distribution(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "web_dataset_distribution" / "valid" + + @property + def invalid_recommended_distribution(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "web_dataset_distribution" / "invalid" + + @property + def valid_web_directory_distribution(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "web_directory_distribution" / "valid" + + @property + def invalid_web_directory_distribution(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "web_directory_distribution" / "invalid" + + @property + def valid_missing_file_local_path(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "missing_file_local_path" / "valid" + + @property + def invalid_missing_file_local_path(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "missing_file_local_path" / "invalid" + + @property + def valid_data_entity_license_divergence(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "data_entity_license" / "valid" + + @property + def invalid_data_entity_license_divergence(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "data_entity_license" / "invalid" + + @property + def valid_recommended_contentSize(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "recommended_contentSize" / "valid" + + @property + def invalid_recommended_contentSize(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "recommended_contentSize" / "invalid" + + @property + def valid_recommended_conformsto(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "recommended_conformsto" / "valid" + + @property + def invalid_recommended_conformsto(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "recommended_conformsto" / "invalid" + + @property + def valid_recommended_sdDatePublished(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "recommended_sdDatePublished" / "valid" + + @property + def invalid_recommended_sdDatePublished(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "recommended_sdDatePublished" / "invalid" + + # 4.3 Dataset (Directory) Data Entity — trailing slash + @property + def valid_recommended_dataset_trailing_slash(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "dataset_trailing_slash" / "valid" + + @property + def invalid_recommended_dataset_trailing_slash(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "dataset_trailing_slash" / "invalid" + + # 4.3 Dataset (Directory) Data Entity — hasPart + @property + def valid_recommended_dataset_has_part(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "dataset_has_part" / "valid" + + @property + def invalid_recommended_dataset_has_part(self) -> Path: + return self.DATA_ENTITIES_CRATES_PATH / "dataset_has_part" / "invalid" + + +class WorkflowsScripts: + WORKFLOWS_SCRIPTS_CRATES_PATH = BASE_PATH / "11_workflows_scripts" + + # --- Script type --- + @property + def valid_script_type(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "script_type" / "valid" + + @property + def invalid_script_type(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "script_type" / "invalid" + + # --- Script name --- + @property + def valid_script_name(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "script_name" / "valid" + + @property + def invalid_script_name(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "script_name" / "invalid" + + # --- Workflow type --- + @property + def valid_workflow_type(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "workflow_type" / "valid" + + @property + def invalid_workflow_type_missing_file(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "workflow_type" / "invalid_missing_file" + + @property + def invalid_workflow_type_missing_ssc(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "workflow_type" / "invalid_missing_ssc" + + # --- Workflow name --- + @property + def valid_workflow_name(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "workflow_name" / "valid" + + @property + def invalid_workflow_name(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "workflow_name" / "invalid" + + # --- programmingLanguage --- + @property + def valid_programming_language(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "programming_language" / "valid" + + @property + def invalid_programming_language(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "programming_language" / "invalid" + + # --- Workflow conformsTo --- + @property + def valid_workflow_conformsTo(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "workflow_conformsTo" / "valid" + + @property + def invalid_workflow_conformsTo(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "workflow_conformsTo" / "invalid" + + # --- ImageObject encodingFormat --- + @property + def valid_image_encoding_format(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "image_encoding_format" / "valid" + + @property + def invalid_image_encoding_format(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "image_encoding_format" / "invalid" + + # --- ImageObject about --- + @property + def valid_image_about(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "image_about" / "valid" + + @property + def invalid_image_about(self) -> Path: + return self.WORKFLOWS_SCRIPTS_CRATES_PATH / "image_about" / "invalid" + + +class ContextualEntities: + CONTEXTUAL_ENTITIES_CRATES_PATH = BASE_PATH / "10_metadata_contextualEntities" + + # --- License entity: SHOULD be typed as CreativeWork --- + @property + def valid_license_entity(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "license_entity" / "valid" + + @property + def invalid_license_entity_no_type(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "license_entity" / "invalid_no_type" + + @property + def invalid_license_entity_no_url(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "license_entity" / "invalid_no_url" + + @property + def invalid_license_entity_no_name(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "license_entity" / "invalid_no_name" + + @property + def invalid_license_entity_no_description(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "license_entity" / "invalid_no_description" + + # --- Organization entity: SHOULD have ROR @id and contactPoint --- + @property + def valid_organization_entity(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "organization_entity" / "valid" + + @property + def invalid_organization_no_ror_id(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "organization_entity" / "invalid_no_ror_id" + + @property + def invalid_organization_no_contactpoint(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "organization_entity" / "invalid_org_no_contactpoint" + + @property + def invalid_organization_contactpoint_no_entity(self) -> Path: + return ( + self.CONTEXTUAL_ENTITIES_CRATES_PATH / "organization_entity" / "invalid_contactpoint_no_contactpoint_entity" + ) + + @property + def invalid_no_author_publisher_contactpoint(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "organization_entity" / "invalid_no_author_publisher_contactpoint" + + # --- Person entity: SHOULD have ORCID @id and valid affiliation --- + @property + def valid_person_entity(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "person_entity" / "valid" + + @property + def invalid_person_no_orcid(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "person_entity" / "invalid_no_orcid" + + @property + def invalid_person_affiliation_not_org(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "person_entity" / "invalid_affiliation_not_org" + + # --- Any Contextual Entity: SHOULD have absolute URI or '#'-prefixed @id --- + @property + def valid_contextual_entity_id_format(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "contextual_entity_id_format" / "valid" + + @property + def invalid_contextual_entity_bare_contactpoint(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "contextual_entity_id_format" / "invalid_bare_contactpoint" + + @property + def invalid_contextual_entity_bare_propertyvalue(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "contextual_entity_id_format" / "invalid_bare_propertyvalue" + + # --- SoftwareApplication / ComputerLanguage: MUST have name, url, version (5.7) --- + @property + def valid_software_application(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "software_application" / "valid" + + @property + def invalid_software_application_no_version(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "software_application" / "invalid_no_version" + + @property + def invalid_software_application_no_name(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "software_application" / "invalid_no_name" + + @property + def invalid_software_application_no_url(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "software_application" / "invalid_no_url" + + @property + def valid_computer_language(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "software_application" / "valid_computer_language" + + @property + def valid_computer_language_with_alternatename(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "software_application" / "valid_with_alternatename" + + # --- Encoding Format entity: MAY include `WebPageElement` when @id has a fragment (5.8) --- + @property + def valid_encoding_format_webpageelement(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "encoding_format" / "valid" + + @property + def info_encoding_format_no_webpageelement(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "encoding_format" / "info_no_webpageelement" + + @property + def encoding_format_no_fragment(self) -> Path: + return self.CONTEXTUAL_ENTITIES_CRATES_PATH / "encoding_format" / "no_fragment" + + +class ReferencedROCrates: + REFERENCED_ROCRATES_CRATES_PATH = CRATES_DATA_PATH / "rocrate-1.3" / "9_referenced_rocrate" + + @property + def valid(self) -> Path: + return self.REFERENCED_ROCRATES_CRATES_PATH / "valid" + + @property + def invalid_no_versionless_conformsto(self) -> Path: + return self.REFERENCED_ROCRATES_CRATES_PATH / "invalid_no_versionless_conformsto" + + @property + def invalid_root_conformsto_versionless(self) -> Path: + return self.REFERENCED_ROCRATES_CRATES_PATH / "invalid_root_conformsto_versionless" + + @property + def invalid_no_subjectof(self) -> Path: + return self.REFERENCED_ROCRATES_CRATES_PATH / "invalid_no_subjectof" + + @property + def invalid_md_encoding_format(self) -> Path: + return self.REFERENCED_ROCRATES_CRATES_PATH / "invalid_md_encoding_format" + + @property + def invalid_md_conformsto(self) -> Path: + return self.REFERENCED_ROCRATES_CRATES_PATH / "invalid_md_conformsto" + + @property + def invalid_md_about(self) -> Path: + return self.REFERENCED_ROCRATES_CRATES_PATH / "invalid_md_about" + + @property + def invalid_missing_sddatepublished(self) -> Path: + return self.REFERENCED_ROCRATES_CRATES_PATH / "invalid_missing_sddatepublished" + + @property + def valid_with_identifier(self) -> Path: + return self.REFERENCED_ROCRATES_CRATES_PATH / "valid_with_identifier" + + @property + def valid_with_relative_path(self) -> Path: + return self.REFERENCED_ROCRATES_CRATES_PATH / "valid_with_relative_path" + + +class ValidROCrate13: + base_path = VALID_CRATES_DATA_PATH + + @property + def attached(self) -> Path: + return self.base_path / "ro-crate-1.3-attached" + + @property + def attached_absolute_root(self) -> Path: + return self.base_path / "ro-crate-1.3-absolute-root" + + @property + def detached(self) -> Path: + return self.base_path / "detached-1.3" / "dataset-ro-crate-metadata.json" + + @property + def detached_prefixed(self) -> Path: + return self.base_path / "detached-1.3" / "test-ro-crate-metadata.json" + + +class InvalidROCrate13: + base_path = INVALID_CRATES_DATA_PATH / "ro-crate-1.3" + + @property + def invalid_context(self) -> Path: + return self.base_path / "invalid-context" + + @property + def invalid_date_published(self) -> Path: + return self.base_path / "invalid-date-published" + + @property + def detached_relative_entity(self) -> Path: + return self.base_path / "detached-relative-entity" / "dataset-ro-crate-metadata.json" + + @property + def detached_bad_filename(self) -> Path: + return self.base_path / "detached-bad-filename" / "ro-crate-metadata.json" diff --git a/tests/shared.py b/tests/shared.py index 06585b9a7..b9c6c6801 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -29,7 +29,6 @@ import rdflib from rocrate_validator import models, services -from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER logger = logging.getLogger(__name__) @@ -43,6 +42,25 @@ PREFIX dct: """ +# Identifier of the RO-Crate 1.1 profile. It is the profile assumed by the tests +# that exercise 1.1 semantics on the 1.1 crate corpus, and the default used by +# `do_entity_test()`; tests that target another profile (e.g. 1.3) pass its +# identifier explicitly. +# +# This is a hard-coded literal on purpose, NOT `DEFAULT_PROFILE_IDENTIFIER`: the +# validator's default profile changes over time (it is 1.3 today), while the tests +# that use this constant must keep targeting 1.1 even when the default moves on. +RO_CRATE_1_1_PROFILE_IDENTIFIER = "ro-crate-1.1" + +# JSON-LD context to use when re-serialising a crate patched via SPARQL, per +# profile. Profiles that are not listed inherit from RO-Crate 1.1 and use its context. +_PROFILE_CONTEXT_URIS = { + "ro-crate-1.1": "https://w3id.org/ro/crate/1.1/context", + "ro-crate-1.2": "https://w3id.org/ro/crate/1.2/context", + "ro-crate-1.3": "https://w3id.org/ro/crate/1.3/context", +} +_DEFAULT_CONTEXT_URI = _PROFILE_CONTEXT_URIS[RO_CRATE_1_1_PROFILE_IDENTIFIER] + def first(c: Collection[T]) -> T: return next(iter(c)) @@ -103,10 +121,22 @@ def _uses_https_schema(graph: rdflib.Graph) -> bool: return False +def _context_uri_for_profile(profile_identifier: str) -> str: + """ + Return the JSON-LD context matching the profile under test. + + Keeping the context aligned with the profile matters: re-serialising a 1.1 + context while validating against 1.3 trips the `ro-crate-1.3_4.2` check and + the test would fail for a reason unrelated to what it is asserting. + """ + return _PROFILE_CONTEXT_URIS.get(profile_identifier, _DEFAULT_CONTEXT_URI) + + def _prepare_temp_rocrate( rocrate_path: Path, rocrate_entity_patch: dict | None, rocrate_entity_mod_sparql: str | None, + context_uri: str = _DEFAULT_CONTEXT_URI, ) -> Path: # `mkdtemp` returns a stable path the test owns; `TemporaryDirectory().name` # was deleted on GC before copytree ran. `dirs_exist_ok=True` lets us copy @@ -131,8 +161,6 @@ def _prepare_temp_rocrate( "https://schema.org/", ) rocrate_graph.update(rocrate_entity_mod_sparql) - # preserve the original context to avoid forcing a downgrade/upgrade - context_uri = rocrate.get("@context", "https://w3id.org/ro/crate/1.2/context") rocrate_graph.serialize( Path(temp_rocrate_path, "ro-crate-metadata.json"), format="json-ld", @@ -143,7 +171,7 @@ def _prepare_temp_rocrate( return temp_rocrate_path -def do_entity_test( # pylint: disable=too-many-locals +def do_entity_test( # noqa: PLR0912 # Shared test driver: the long signature mirrors `ValidationSettings` # plus the assertion-shaping knobs, so locals scale with the surface area # it has to cover. Splitting it would only push the locals to the caller. @@ -153,7 +181,7 @@ def do_entity_test( # pylint: disable=too-many-locals expected_triggered_requirements: list[str] | None = None, expected_triggered_issues: list[str] | None = None, abort_on_first: bool = False, - profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER, + profile_identifier: str = RO_CRATE_1_1_PROFILE_IDENTIFIER, rocrate_entity_patch: dict | None = None, rocrate_entity_mod_sparql: str | None = None, skip_checks: list[str] | None = None, @@ -184,7 +212,12 @@ def do_entity_test( # pylint: disable=too-many-locals and isinstance(rocrate_path, Path) and rocrate_path.is_dir() ): - temp_rocrate_path = _prepare_temp_rocrate(rocrate_path, rocrate_entity_patch, rocrate_entity_mod_sparql) + temp_rocrate_path = _prepare_temp_rocrate( + rocrate_path, + rocrate_entity_patch, + rocrate_entity_mod_sparql, + context_uri=_context_uri_for_profile(profile_identifier), + ) rocrate_path = temp_rocrate_path if expected_triggered_requirements is None: @@ -220,6 +253,14 @@ def do_entity_test( # pylint: disable=too-many-locals requirement_severity, result.context.requirement_severity, ) + + if expected_validation_result and result.has_issues(): + for issue in result.get_issues(): + logger.debug( + f"Detected issue of severity {issue.severity.name} " + f'with check "{issue.check.identifier}": {issue.message}' + ) + assert result.passed() == expected_validation_result, ( f"RO-Crate should be {'valid' if expected_validation_result else 'invalid'}" ) diff --git a/tests/test_cli.py b/tests/test_cli.py index 5f0cdef3d..a9b393841 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -14,20 +14,28 @@ import json import re +from io import StringIO from pathlib import Path +from types import SimpleNamespace from unittest.mock import patch from click.testing import CliRunner -from pytest import fixture +from pytest import fixture, mark +from rdflib import Graph from rocrate_validator import services +from rocrate_validator.cli.commands.validate import _resolve_profile_identifiers from rocrate_validator.cli.main import cli +from rocrate_validator.errors import BadSyntaxError +from rocrate_validator.models import ProfileDetectionDiagnostic, ProfileDetectionResult, ValidationContext from rocrate_validator.requirements.python import PyFunctionCheck from rocrate_validator.requirements.shacl.checks import SHACLCheck from rocrate_validator.utils import log as logging +from rocrate_validator.utils.io_helpers.output.console import Console from rocrate_validator.utils.versioning import get_version from tests.conftest import SKIP_LOCAL_DATA_ENTITY_EXISTENCE_CHECK_IDENTIFIER -from tests.ro_crates import InvalidFileDescriptor, ValidROC +from tests.ro_crates import InvalidFileDescriptor, InvalidFileDescriptorEntity, ValidROC +from tests.ro_crates_v1_3 import ValidROCrate13 # set up logging logger = logging.getLogger(__name__) @@ -48,6 +56,106 @@ def test_version(cli_runner: CliRunner): assert get_version() in result.output +def test_validate_packaging_mode_option(cli_runner: CliRunner, tmp_path: Path): + help_result = cli_runner.invoke(cli, ["validate", "--help"]) + assert help_result.exit_code == 0 + assert "--packaging-mode" in help_result.output + + invalid_choice = cli_runner.invoke(cli, ["validate", "--packaging-mode", "unknown"]) + assert invalid_choice.exit_code != 0 + assert "auto" in invalid_choice.output + + (tmp_path / "ro-crate-metadata.json").write_text("{}") + incompatible = cli_runner.invoke( + cli, + ["validate", str(tmp_path), "--packaging-mode", "detached", "--no-auto-profile", "--no-paging"], + ) + assert incompatible.exit_code != 0 + assert "standalone metadata document" in incompatible.output + + +def test_profile_detection_cli_escapes_metadata_values_in_warning(monkeypatch): + """Malformed metadata values containing Rich markup are rendered as plain text.""" + detection = ProfileDetectionResult( + profiles=[], + diagnostics=[ + ProfileDetectionDiagnostic( + code="INVALID_CONFORMS_TO", + property="conformsTo", + location="RO-Crate Metadata Descriptor", + value="[/]", + message="Expected a JSON-LD reference object containing `@id`.", + ) + ], + ) + output = StringIO() + monkeypatch.setattr(services, "detect_profiles_detailed", lambda **kwargs: detection) + + profile_identifiers, _ = _resolve_profile_identifiers( + Console(file=output, width=200), + interactive=False, + no_auto_profile=False, + available_profiles=[], + profile_identifiers=[], + validation_settings={}, + ) + + assert profile_identifiers == ["ro-crate"] + assert "[/]" in output.getvalue() + + +@mark.parametrize("interactive, confirmation", [(False, None), (True, "y"), (True, "n")]) +def test_profile_detection_cli_confirms_string_profile(monkeypatch, interactive, confirmation): + """A string profile URI is used only after interactive confirmation.""" + output = StringIO() + profile = SimpleNamespace(identifier="ro-crate-1.3", uri="https://w3id.org/ro/crate/1.3") + settings = { + "rocrate_uri": "/tmp/string-profile-cli", + "metadata_only": True, + "metadata_dict": { + "@context": {}, + "@graph": [ + {"@id": "ro-crate-metadata.json", "about": {"@id": "./"}, "conformsTo": profile.uri}, + {"@id": "./", "@type": "Dataset"}, + ], + }, + } + profile.inherited_profiles = [] + monkeypatch.setattr("rocrate_validator.models.profile.Profile.load_profiles", lambda *args, **kwargs: [profile]) + prompts = [] + + def confirm(*args, **kwargs): + prompts.append(kwargs["message"]) + return confirmation + + monkeypatch.setattr("rocrate_validator.cli.commands.validate.get_single_char", confirm) + monkeypatch.setattr("rocrate_validator.cli.commands.validate.multiple_choice", lambda *args: []) + identifiers, autodetection = _resolve_profile_identifiers( + Console(file=output, width=200), + interactive=interactive, + no_auto_profile=False, + available_profiles=[profile], + profile_identifiers=[], + validation_settings=settings, + ) + + accepted = interactive and confirmation == "y" + assert identifiers == (["ro-crate-1.3"] if accepted else ["ro-crate"]) + assert autodetection is accepted + assert len(prompts) == int(interactive) + text = output.getvalue() + assert "WARNING: Non-conforming metadata" in text + assert "RO-Crate Metadata Descriptor" in text + assert 'Use {"@id": "https://w3id.org/ro/crate/1.3"} instead.' in text + if interactive: + assert "The string URI matches the available profile ro-crate-1.3" in text + assert "Use this profile for validation?" in prompts[0] + else: + assert "--profile-identifier" in text + if accepted: + assert "Unable to automatically detect" not in text + + def test_validate_subcmd_invalid_rocrate1(cli_runner: CliRunner): result = cli_runner.invoke( cli, @@ -57,12 +165,101 @@ def test_validate_subcmd_invalid_rocrate1(cli_runner: CliRunner): assert result.exit_code == 1 +@mark.parametrize("use_directory", [False, True]) +def test_validate_invalid_json_reports_syntax_location(cli_runner: CliRunner, tmp_path: Path, use_directory): + """Malformed JSON is explained before attempting to select a profile.""" + metadata = tmp_path / "ro-crate-metadata.json" + metadata.write_text( + '{\n "@graph": [{\n "@id": "ro-crate-metadata.json",\n' + ' "conformsTo": "@id": "https://w3id.org/ro/crate/1.3"\n }]\n}' + ) + + result = cli_runner.invoke(cli, ["validate", str(tmp_path if use_directory else metadata), "--no-paging"]) + + assert result.exit_code == 2 + assert "The RO-Crate metadata is not valid JSON." in result.output + assert "line 4, column 24" in result.output + assert "Expecting ',' delimiter" in result.output + assert '"conformsTo": "@id":' in result.output + assert "^" in result.output + assert "Correct the JSON syntax" in result.output + assert "Unexpected error" not in result.output + assert "Unable to automatically detect" not in result.output + + def test_validate_subcmd_valid_local_folder_rocrate(cli_runner: CliRunner): result = cli_runner.invoke(cli, ["validate", str(ValidROC().wrroc_paper_long_date), "--verbose", "--no-paging"]) assert result.exit_code == 0 assert re.search(r"RO-Crate.*is a valid", result.output) +def test_validate_subcmd_pyshacl_engine_failure_exits_with_error(cli_runner: CliRunner, monkeypatch): + """An internal pySHACL failure must never be reported as valid by the CLI.""" + + def fail_validation(*args, **kwargs): + raise ImportError("cannot import name 'ConjunctiveLike' from 'pyshacl.consts'") + + monkeypatch.setattr(ValidationContext, "data_graph", property(lambda self: Graph())) + monkeypatch.setattr("rocrate_validator.requirements.shacl.validator.pyshacl.validate", fail_validation) + result = cli_runner.invoke( + cli, + ["validate", str(ValidROC().wrroc_paper_long_date), "--no-paging", "-p", "ro-crate"], + ) + + assert result.exit_code == 2 + assert "SHACL validation could not be executed" in result.output + assert "ConjunctiveLike" in result.output + assert "is a valid" not in result.output + + +def test_validate_subcmd_profile_syntax_error_is_concise_without_debug(cli_runner: CliRunner, monkeypatch): + """Parser failures must show a concise extra_check without a traceback by default.""" + + def fail_validation(*args, **kwargs): + raise BadSyntaxError( + "at line 26 of <>:\nBad syntax (expected '.') at ^ in:\nsource dump", + "shapes.ttl", + line=26, + character=5, + ) + + monkeypatch.setattr("rocrate_validator.cli.commands.validate.services.validate", fail_validation) + result = cli_runner.invoke( + cli, + ["validate", str(ValidROC().wrroc_paper_long_date), "--no-paging", "-p", "ro-crate"], + ) + + assert result.exit_code == 2 + assert "The validation profile could not be parsed" in result.output + assert "line 26, character 5" in result.output + assert "line 26, character 5 -->" in result.output + assert "Bad syntax (expected '.')" in result.output + assert "Traceback" not in result.output + + +def test_validate_subcmd_profile_syntax_error_renders_traceback_in_debug(cli_runner: CliRunner, monkeypatch): + """Debug mode must render parser exception chains without a Rich renderer crash.""" + + def fail_validation(*args, **kwargs): + try: + Graph().parse(data="not valid turtle", format="turtle") + except Exception as cause: + raise BadSyntaxError(str(cause), "shapes.ttl") from cause + raise AssertionError("The RDF parser unexpectedly accepted invalid Turtle") + + monkeypatch.setattr("rocrate_validator.cli.commands.validate.services.validate", fail_validation) + result = cli_runner.invoke( + cli, + ["--debug", "validate", str(ValidROC().wrroc_paper_long_date), "--no-paging", "-p", "ro-crate"], + ) + + assert result.exit_code == 2 + assert "The validation profile could not be parsed" in result.output + assert "Debug traceback" in result.output + assert "Traceback (most recent call last)" in result.output + assert "TypeError: str or Text instance required" not in result.output + + def test_validate_subcmd_valid_remote_rocrate(cli_runner: CliRunner): result = cli_runner.invoke( cli, @@ -147,6 +344,31 @@ def mock_validate(*args, **kwargs): ) +def test_validate_extra_checks_option(cli_runner: CliRunner): + called_args: list = [] + + def mock_validate(*args, **kwargs): + called_args.extend(args) + + with patch("rocrate_validator.cli.commands.validate.services.validate") as mock_validate_rocrate: + mock_validate_rocrate.return_value = None + mock_validate_rocrate.side_effect = mock_validate + result = cli_runner.invoke( + cli, + [ + "--no-interactive", + "validate", + str(ValidROC().sort_and_change_archive), + "--extra-checks", + "--no-paging", + ], + ) + + assert result.exit_code == 2 + settings = called_args[0] + assert settings["extra_checks"] is True + + def test_validate_output_file_text_report(cli_runner: CliRunner, tmp_path: Path): output_file = tmp_path / "report.txt" result = cli_runner.invoke( @@ -188,6 +410,77 @@ def test_validate_output_file_json_report(cli_runner: CliRunner, tmp_path: Path) json.loads(output_file.read_text(encoding="utf-8")) # must be valid JSON +def test_validate_show_skipped_checks(cli_runner: CliRunner): + result = cli_runner.invoke( + cli, + [ + "validate", + str(InvalidFileDescriptorEntity().invalid_conforms_to), + "--profile-identifier", + "ro-crate-1.1", + "--skip-checks", + "ro-crate-1.1_5.3,ro-crate-1.1_12.1", + "--show-skipped-checks", + "--no-paging", + ], + ) + + assert result.exit_code == 0, result.output + assert "Skipped Checks" in result.output + assert "ro-crate-1.1_5.3" in result.output + assert "configured" in result.output + + +def test_validate_text_report_file_includes_skipped_checks(cli_runner: CliRunner, tmp_path: Path): + output_file = tmp_path / "report.txt" + result = cli_runner.invoke( + cli, + [ + "validate", + str(InvalidFileDescriptorEntity().invalid_conforms_to), + "--profile-identifier", + "ro-crate-1.1", + "--skip-checks", + "ro-crate-1.1_5.3,ro-crate-1.1_12.1", + "--show-skipped-checks", + "--output-file", + str(output_file), + "--no-paging", + ], + ) + + assert result.exit_code == 0, result.output + report = output_file.read_text(encoding="utf-8") + assert "Skipped Checks" in report + assert "ro-crate-1.1_12.1" in report + + +def test_validate_json_report_aggregates_skipped_checks(cli_runner: CliRunner, tmp_path: Path): + output_file = tmp_path / "report.json" + result = cli_runner.invoke( + cli, + [ + "validate", + str(InvalidFileDescriptorEntity().invalid_conforms_to), + "--profile-identifier", + "ro-crate-1.1", + "--skip-checks", + "ro-crate-1.1_5.3,ro-crate-1.1_12.1", + "--output-format", + "json", + "--output-file", + str(output_file), + "--no-paging", + ], + ) + + assert result.exit_code == 0, result.output + payload = json.loads(output_file.read_text(encoding="utf-8")) + assert payload["skipped_checks"] >= 2 + skipped_ids = {detail["identifier"] for detail in payload["skipped_check_details"]} + assert {"ro-crate-1.1_5.3", "ro-crate-1.1_12.1"} <= skipped_ids + + def test_validate_with_invalid_profiles_path_dir(cli_runner: CliRunner): dummy_profiles_path = "/tmp/dummy_profiles" result = cli_runner.invoke( @@ -219,6 +512,65 @@ def test_profiles_list(cli_runner: CliRunner): assert "ro-crate-1.1" in result.output # Check for a known profile +def test_profiles_check(cli_runner: CliRunner) -> None: + """Resolve the bare RO-Crate token to the latest profile in check reports.""" + result = cli_runner.invoke(cli, ["profiles", "check", "ro-crate", "--no-paging"]) + + assert result.exit_code == 0 + assert "Profile checks: ro-crate-1.3" in result.output + assert "unique-requirement-check-identity" in result.output + assert "rule-overlay-consistency" in result.output + assert "PASS" in result.output + + +def test_profiles_check_reports_invalid_profile(cli_runner: CliRunner, fake_profiles_path: Path): + result = cli_runner.invoke( + cli, + [ + "profiles", + "--extra-profiles-path", + str(fake_profiles_path), + "check", + "invalid-duplicated-shapes", + "--no-paging", + ], + ) + + assert result.exit_code == 1 + assert "unique-requirement-check-identity" in result.output + assert "FAIL" in result.output + + +def test_validate_no_auto_profile_falls_back_to_1_3(cli_runner: CliRunner): + """ + With auto-detection disabled the base `ro-crate` profile is used, and that + now resolves to 1.3. + """ + result = cli_runner.invoke( + cli, + ["validate", str(ValidROCrate13().attached), "--no-auto-profile", "--no-paging"], + ) + assert result.exit_code == 0 + assert "ro-crate-1.3" in result.output + + +def test_validate_auto_profile_still_uses_the_declared_profile(cli_runner: CliRunner): + """ + Auto-detection keeps a 1.1 crate on the 1.1 profile: the new default only + applies when no profile can be detected. + """ + result = cli_runner.invoke(cli, ["validate", str(ValidROC().wrroc_paper), "--no-paging"]) + assert result.exit_code == 0 + assert "ro-crate-1.1" in result.output + + +def test_profiles_describe_defaults_to_1_3(cli_runner: CliRunner): + """`profiles describe` with no argument describes the default profile.""" + result = cli_runner.invoke(cli, ["profiles", "describe", "--no-paging"]) + assert result.exit_code == 0 + assert "Profile: ro-crate-1.3" in result.output + + def test_extra_profiles_list(cli_runner: CliRunner, fake_profiles_path: Path): """ Test the list of extra profiles. @@ -274,6 +626,8 @@ def test_profiles_describe_verbose(cli_runner: CliRunner): result = cli_runner.invoke(cli, ["profiles", "describe", _DESCRIBE_TEST_PROFILE, "-v", "--no-paging"]) assert result.exit_code == 0 assert check.identifier in result.output + assert "Effective ID" in result.output + assert "Source ID" in result.output def test_describe_check_relative_id(cli_runner: CliRunner): @@ -298,7 +652,7 @@ def test_describe_check_unknown(cli_runner: CliRunner): """An out-of-range check id produces a usage error with a hint.""" result = cli_runner.invoke(cli, ["profiles", "describe", _DESCRIBE_TEST_PROFILE, "99.99", "--no-paging"]) assert result.exit_code == 2 - assert "No requirement #99" in result.output + assert "No effective check '99.99'" in result.output def test_describe_check_bad_format(cli_runner: CliRunner): @@ -314,7 +668,51 @@ def test_describe_check_profile_mismatch(cli_runner: CliRunner): cli, ["profiles", "describe", _DESCRIBE_TEST_PROFILE, "some-other-profile_1.1", "--no-paging"] ) assert result.exit_code == 2 - assert "does not belong to profile" in result.output + assert "is not part of effective profile" in result.output + + +def test_describe_overlay_profile_distinguishes_inherited_and_replaced_checks(cli_runner: CliRunner): + result = cli_runner.invoke( + cli, + [ + "profiles", + "--profiles-path", + "tests/data/profiles/effective_checks", + "describe", + "effective-b", + "-v", + "--no-paging", + ], + env={"COLUMNS": "160"}, + ) + + assert result.exit_code == 0, result.output + assert "Replaces effective-a_1.2" in result.output + assert "Inherited from effective-a" in result.output + assert "effective-b_1.1" in result.output + assert "effective-a_1.1" in result.output + + +def test_describe_inherited_overlay_check_shows_provenance(cli_runner: CliRunner): + result = cli_runner.invoke( + cli, + [ + "profiles", + "--profiles-path", + "tests/data/profiles/effective_checks", + "describe", + "effective-b", + "1.1", + "--no-paging", + ], + env={"COLUMNS": "160"}, + ) + + assert result.exit_code == 0, result.output + assert "Relation: Inherited from effective-a" in result.output + assert "Effective ID: effective-b_1.1" in result.output + assert "Source ID: effective-a_1.1" in result.output + assert "Source profile: effective-a" in result.output def test_describe_check_verbose_python(cli_runner: CliRunner): diff --git a/tests/test_models.py b/tests/test_models.py index cd580e07b..556745c9d 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -12,11 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +from pathlib import Path + import pytest from rocrate_validator import models, services from rocrate_validator.models import URI, LevelCollection, RequirementLevel, Severity, ValidationSettings -from tests.ro_crates import InvalidRootDataEntity, WROCInvalidReadme +from rocrate_validator.requirements.python import PyFunctionCheck +from tests.ro_crates import InvalidRootDataEntity, ValidROC, WROCInvalidReadme def test_severity_ordering(): @@ -95,6 +98,55 @@ def test_sortability_checks(validation_settings: ValidationSettings): assert one.requirement >= two.requirement +def test_validation_statistics_count_final_check_statuses(validation_settings: ValidationSettings): + """Repeated intermediate skip events must not inflate final check counts.""" + validation_settings.rocrate_uri = URI(str(ValidROC().wrroc_paper)) + validation_settings.profile_identifier = "ro-crate-1.2" + validation_settings.requirement_severity = Severity.REQUIRED + validation_settings.abort_on_first = True + + result = services.validate(validation_settings) + statistics = result.statistics + + assert statistics.total_skipped_checks == result.skipped_checks_count + assert len({check.identifier for check in statistics.skipped_checks}) == statistics.total_skipped_checks + assert set(statistics.skipped_checks) == result.skipped_checks + assert set(statistics.skipped_checks).isdisjoint(statistics.validated_checks) + assert len(statistics.validated_checks) + statistics.total_skipped_checks == statistics.total_checks + + +def test_validation_statistics_preserve_failed_overlay_check_on_abort( + validation_settings: ValidationSettings, monkeypatch: pytest.MonkeyPatch +): + """Fail-fast must preserve a replacement that failed in its source-profile slot.""" + validation_settings.rocrate_uri = URI(str(ValidROC().wrroc_paper)) + validation_settings.profiles_path = Path(__file__).parent / "data" / "profiles" / "effective_checks" + validation_settings.profile_identifier = "effective-b" + validation_settings.requirement_severity = Severity.REQUIRED + validation_settings.abort_on_first = True + execute_check = PyFunctionCheck.execute_check + + def fail_overlay_check(check, context): + if check.requirement.profile.identifier == "effective-b" and check.name == "Shared check": + context.result.add_issue("Overlay replacement failed", check) + return False + return execute_check(check, context) + + monkeypatch.setattr(PyFunctionCheck, "execute_check", fail_overlay_check) + + result = services.validate(validation_settings) + statistics = result.statistics + + assert not result.passed() + assert len(result.failed_checks) == 1 + failed_check = next(iter(result.failed_checks)) + assert statistics.failed_checks == [failed_check] + assert result.get_check_result(failed_check) is models.CheckResult.FAILED + assert failed_check in statistics.validated_checks + assert failed_check in result.executed_checks + assert statistics.total_skipped_checks == result.skipped_checks_count == 0 + + def test_sortability_issues(validation_settings: ValidationSettings): validation_settings.rocrate_uri = URI(str(WROCInvalidReadme().wroc_readme_wrong_encoding_format)) result: models.ValidationResult = services.validate(validation_settings) diff --git a/tests/unit/requirements/shacl/transformers/test_base.py b/tests/unit/requirements/shacl/transformers/test_base.py new file mode 100644 index 000000000..b49a16c85 --- /dev/null +++ b/tests/unit/requirements/shacl/transformers/test_base.py @@ -0,0 +1,109 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import abstractmethod + +import pytest +from rdflib import Graph, Literal, Namespace + +from rocrate_validator.requirements.shacl.transformers import GraphTransformer, graph_transformer, registry +from rocrate_validator.requirements.shacl.transformers.pipeline import run_registered_transformers +from rocrate_validator.requirements.shacl.transformers.registry import ( + get_registered_transformers, +) + +EXAMPLE = Namespace("https://example.org/") + + +def test_abstract_transformers_are_not_registered(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(registry, "_TRANSFORMERS", {}) + + class _AbstractTransformer(GraphTransformer): + @abstractmethod + def transform(self, data_graph: Graph) -> Graph: + raise NotImplementedError + + assert not get_registered_transformers() + + +def test_registered_transformers_run_in_order(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(registry, "_TRANSFORMERS", {}) + trace = EXAMPLE.trace + step = EXAMPLE.step + + @graph_transformer(identifier=EXAMPLE.function_transformer, order=20) + def function_transformer(data_graph: Graph) -> Graph: + assert (trace, step, Literal("class")) in data_graph + data_graph.add((trace, step, Literal("function"))) + return data_graph + + class _ClassTransformer(GraphTransformer): + identifier = EXAMPLE.class_transformer + order = 10 + + def transform(self, data_graph: Graph) -> Graph: + data_graph.add((trace, step, Literal("class"))) + return data_graph + + source = Graph() + transformed = run_registered_transformers(source) + + assert (trace, step, Literal("class")) in transformed + assert (trace, step, Literal("function")) in transformed + assert [item.order for item in get_registered_transformers()] == [10, 20] + + +def test_transformer_must_return_an_rdflib_graph(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(registry, "_TRANSFORMERS", {}) + + @graph_transformer(identifier=EXAMPLE.invalid_transformer) + def invalid_transformer(data_graph: Graph) -> Graph: + del data_graph + return "not a graph" # type: ignore[return-value] + + with pytest.raises(TypeError, match="did not return an RDFLib Graph"): + run_registered_transformers(Graph()) + + +def test_only_requested_transformers_run(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(registry, "_TRANSFORMERS", {}) + + @graph_transformer(identifier=EXAMPLE.first_transformer, order=10) + def first_transformer(data_graph: Graph) -> Graph: + data_graph.add((EXAMPLE.trace, EXAMPLE.step, Literal("first"))) + return data_graph + + @graph_transformer(identifier=EXAMPLE.second_transformer, order=20) + def second_transformer(data_graph: Graph) -> Graph: + data_graph.add((EXAMPLE.trace, EXAMPLE.step, Literal("second"))) + return data_graph + + transformed = run_registered_transformers(Graph(), {EXAMPLE.second_transformer}) + + assert (EXAMPLE.trace, EXAMPLE.step, Literal("first")) not in transformed + assert (EXAMPLE.trace, EXAMPLE.step, Literal("second")) in transformed + + +def test_transformer_identifiers_must_be_unique(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(registry, "_TRANSFORMERS", {}) + + @graph_transformer(identifier=EXAMPLE.duplicate) + def first_transformer(data_graph: Graph) -> Graph: + return data_graph + + with pytest.raises(ValueError, match="already registered"): + + @graph_transformer(identifier=EXAMPLE.duplicate) + def second_transformer(data_graph: Graph) -> Graph: + return data_graph diff --git a/tests/unit/requirements/shacl/transformers/test_preparation.py b/tests/unit/requirements/shacl/transformers/test_preparation.py new file mode 100644 index 000000000..2aed569f4 --- /dev/null +++ b/tests/unit/requirements/shacl/transformers/test_preparation.py @@ -0,0 +1,75 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rdflib import RDF, BNode, Graph, Literal, Namespace +from rdflib.collection import Collection +from rdflib.namespace import SH + +from rocrate_validator.requirements.shacl.transformers import REQUIRES_GRAPH_TRANSFORMER_PREDICATE +from rocrate_validator.requirements.shacl.transformers.preparation import prepare_validation_graphs +from rocrate_validator.transformers.validation_candidate_marker import ( + VALIDATION_CANDIDATE_PREDICATE, + VALIDATION_CANDIDATE_TRANSFORMER, +) + +EXAMPLE = Namespace("https://example.org/") +SCHEMA = Namespace("http://schema.org/") + + +def test_unrequested_transformers_do_not_copy_graph_objects(): + data_graph = Graph() + data_graph.add((EXAMPLE.entity, SCHEMA.name, Literal("Entity"))) + shapes_graph = Graph() + shapes_graph.add((EXAMPLE.shape, RDF.type, SH.NodeShape)) + + prepared = prepare_validation_graphs(data_graph, shapes_graph) + + assert prepared.data_graph is data_graph + assert prepared.shapes_graph is shapes_graph + + +def test_requested_transformers_use_isolated_data_and_shapes_graphs(): + data_graph = Graph() + data_graph.add((EXAMPLE.entity, SCHEMA.name, Literal("Entity"))) + shapes_graph = Graph() + shapes_graph.add((EXAMPLE.shape, RDF.type, SH.NodeShape)) + shapes_graph.add((EXAMPLE.shape, SH.closed, Literal(True))) + target = BNode() + shapes_graph.add((EXAMPLE.shape, SH.target, target)) + shapes_graph.add((target, RDF.type, SH.SPARQLTarget)) + shapes_graph.add((target, SH.select, Literal("SELECT ?this WHERE { ?this schema:name ?name }"))) + shapes_graph.add( + ( + EXAMPLE.shape, + REQUIRES_GRAPH_TRANSFORMER_PREDICATE, + VALIDATION_CANDIDATE_TRANSFORMER, + ) + ) + original_data = set(data_graph) + original_shapes = set(shapes_graph) + + prepared = prepare_validation_graphs(data_graph, shapes_graph) + + assert prepared.data_graph is not data_graph + assert prepared.shapes_graph is not shapes_graph + assert (EXAMPLE.entity, VALIDATION_CANDIDATE_PREDICATE, Literal(True)) in prepared.data_graph + assert (EXAMPLE.entity, VALIDATION_CANDIDATE_PREDICATE, None) not in data_graph + assert (EXAMPLE.shape, SH.ignoredProperties, None) not in shapes_graph + assert set(data_graph) == original_data + assert set(shapes_graph) == original_shapes + + assert isinstance(prepared.shapes_graph, Graph) + ignored_head = prepared.shapes_graph.value(EXAMPLE.shape, SH.ignoredProperties) + assert isinstance(ignored_head, BNode) + assert VALIDATION_CANDIDATE_PREDICATE in Collection(prepared.shapes_graph, ignored_head) diff --git a/tests/unit/requirements/test_dependencies.py b/tests/unit/requirements/test_dependencies.py new file mode 100644 index 000000000..84f15ce5f --- /dev/null +++ b/tests/unit/requirements/test_dependencies.py @@ -0,0 +1,153 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from rocrate_validator.errors import CheckDependencyError +from rocrate_validator.models import CheckResult, RequirementCheck, RequirementLoader, Severity + + +class _Profile: + def __init__(self, identifier: str = "test", parents: tuple["_Profile", ...] = ()) -> None: + """Create a minimal profile with optional inherited parents.""" + self.identifier = identifier + self.parents = list(parents) + self.requirements = [] + + @property + def inherited_profiles(self) -> list["_Profile"]: + """Return the profiles contributing checks to this test profile.""" + return self.parents + + def get_requirement_check(self, name: str, severity: Severity | None = None) -> RequirementCheck | None: + """Return the unique local check matching ``name`` and ``severity``.""" + matches = [ + check + for requirement in self.requirements + for check in requirement.get_checks() + if check.name == name and (severity is None or check.severity == severity) + ] + assert len(matches) <= 1 + return matches[0] if matches else None + + +class _Requirement: + def __init__(self, profile, name): + self.profile = profile + self.name = name + self._checks = [] + profile.requirements.append(self) + + def get_checks(self): + return self._checks.copy() + + def set_checks_order(self, checks): + self._checks = checks.copy() + for order_number, check in enumerate(self._checks, start=1): + check.order_number = order_number + + +class _Check(RequirementCheck): + def execute_check(self, context): + return CheckResult.PASSED + + +def _check(requirement, name, depends_on=()): + check = _Check(requirement, name, depends_on=depends_on) + requirement._checks.append(check) + return check + + +def test_order_by_dependencies_reorders_checks_within_a_requirement(): + profile = _Profile() + requirement = _Requirement(profile, "Requirement") + dependent = _check(requirement, "dependent", ("base",)) + base = _check(requirement, "base") + + RequirementLoader.order_by_dependencies([requirement]) + + assert requirement.get_checks() == [base, dependent] + assert [check.order_number for check in requirement.get_checks()] == [1, 2] + + +def test_order_by_dependencies_reorders_requirements(): + profile = _Profile() + dependent_requirement = _Requirement(profile, "Dependent") + dependency_requirement = _Requirement(profile, "Dependency") + dependent = _check(dependent_requirement, "dependent", ("base",)) + base = _check(dependency_requirement, "base") + + assert RequirementLoader.order_by_dependencies([dependent_requirement, dependency_requirement]) == [ + dependency_requirement, + dependent_requirement, + ] + assert dependent.depends_on == ("base",) + assert not base.depends_on + + +def test_inherited_dependency_is_resolved_from_effective_profile() -> None: + """Resolve a target check dependency supplied by an overlay source.""" + source = _Profile("source") + target = _Profile("target", parents=(source,)) + source_requirement = _Requirement(source, "Source") + target_requirement = _Requirement(target, "Target") + dependency = _check(source_requirement, "base") + _check(target_requirement, "dependent", ("base",)) + + assert RequirementLoader.order_by_dependencies([target_requirement], profile=target) == [target_requirement] + + closure = RequirementLoader.dependency_closure([target_requirement]) + assert set(closure) == {source_requirement, target_requirement} + assert RequirementLoader.effective_check_index(target)["base"] == [dependency] + + with pytest.raises(CheckDependencyError, match="was not selected"): + RequirementLoader.dependency_closure([target_requirement], include_dependencies=False) + + +@pytest.mark.parametrize( + ("dependency_name", "message"), + [ + ("missing", "unknown check"), + ], +) +def test_order_by_dependencies_rejects_unknown_dependencies(dependency_name, message): + profile = _Profile() + requirement = _Requirement(profile, "Requirement") + _check(requirement, "dependent", (dependency_name,)) + + with pytest.raises(CheckDependencyError, match=message): + RequirementLoader.order_by_dependencies([requirement]) + + +def test_order_by_dependencies_rejects_ambiguous_dependencies(): + profile = _Profile() + dependent_requirement = _Requirement(profile, "Dependent") + first_dependency = _Requirement(profile, "First") + second_dependency = _Requirement(profile, "Second") + _check(dependent_requirement, "dependent", ("base",)) + _check(first_dependency, "base") + _check(second_dependency, "base") + + with pytest.raises(CheckDependencyError, match="ambiguous check"): + RequirementLoader.order_by_dependencies(profile.requirements) + + +def test_order_by_dependencies_rejects_cycles(): + profile = _Profile() + requirement = _Requirement(profile, "Requirement") + _check(requirement, "first", ("second",)) + _check(requirement, "second", ("first",)) + + with pytest.raises(CheckDependencyError, match="dependency cycle"): + RequirementLoader.order_by_dependencies([requirement]) diff --git a/tests/unit/requirements/test_load_requirements.py b/tests/unit/requirements/test_load_requirements.py index 577bae5a8..70f72a267 100644 --- a/tests/unit/requirements/test_load_requirements.py +++ b/tests/unit/requirements/test_load_requirements.py @@ -16,10 +16,13 @@ from pathlib import Path from typing import Any -from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER -from rocrate_validator.models import LevelCollection, Profile, Severity +import pytest + +from rocrate_validator.errors import CheckDependencyError +from rocrate_validator.models import LevelCollection, Profile, RequirementLoader, Severity from rocrate_validator.requirements.shacl.requirements import SHACLRequirement from tests.ro_crates import InvalidFileDescriptorEntity +from tests.shared import RO_CRATE_1_1_PROFILE_IDENTIFIER # set up logging logger = logging.getLogger(__name__) @@ -108,8 +111,11 @@ def test_order_of_loaded_profile_requirements(profiles_path: str): # The number of profiles should be greater than 0 assert len(profiles) > 0 - # The first profile should be the default profile - assert profiles[0].identifier == DEFAULT_PROFILE_IDENTIFIER + # The first profile should be the base RO-Crate 1.1 profile: it is the root of + # the inheritance graph, and profiles are ordered parents-first. This is a + # literal rather than `DEFAULT_PROFILE_IDENTIFIER` because the assertion is + # about load *ordering*, not about which profile happens to be the default. + assert profiles[0].identifier == RO_CRATE_1_1_PROFILE_IDENTIFIER # Get the first profile profile = profiles[0] @@ -126,17 +132,7 @@ def test_order_of_loaded_profile_requirements(profiles_path: str): requirement.path, ) - # Sort requirements by their order - requirements = sorted( - requirements, - key=lambda x: ( - -(x.severity_from_path.value if x.severity_from_path else 0), - x.path.name if x.path else "", - x.name, - ), - ) - - # Check the order of the requirements + # Requirements are loaded in stable dependency-aware order. for i, requirement in enumerate(requirements): if i < len(requirements) - 1: assert requirement < requirements[i + 1] @@ -164,6 +160,36 @@ def test_order_of_loaded_profile_requirements(profiles_path: str): assert requirement_check.severity == Severity.RECOMMENDED, "The severity of the requirement check is incorrect" +def test_check_dependencies_are_ordered_and_closed(profiles_path: str) -> None: + """Resolve the 1.3 context check dependencies through its 1.2 overlay source.""" + profiles = Profile.load_profiles(profiles_path=profiles_path, severity=Severity.REQUIRED) + profile = next(profile for profile in profiles if profile.identifier == "ro-crate-1.3") + source_profile = next(profile for profile in profiles if profile.identifier == "ro-crate-1.2") + + source_requirements = {requirement.name: requirement for requirement in source_profile.requirements} + existence = source_requirements["File Descriptor existence"] + encoding = source_requirements["File Descriptor UTF-8 encoding"] + json_format = source_requirements["File Descriptor JSON format"] + context = next( + requirement for requirement in profile.requirements if requirement.name == "File Descriptor JSON-LD format" + ) + + ordered_names = [requirement.name for requirement in source_profile.requirements] + assert ordered_names.index(existence.name) < ordered_names.index(encoding.name) + assert ordered_names.index(encoding.name) < ordered_names.index(json_format.name) + + closure = RequirementLoader.dependency_closure([context]) + assert {requirement.name for requirement in closure} == { + existence.name, + encoding.name, + json_format.name, + context.name, + } + + with pytest.raises(CheckDependencyError, match="was not selected"): + RequirementLoader.dependency_closure([context], include_dependencies=False) + + def test_hidden_requirements(profiles_loading_hidden_requirements: str): # Define the list of requirements names diff --git a/tests/unit/requirements/test_profile_namespaces.py b/tests/unit/requirements/test_profile_namespaces.py new file mode 100644 index 000000000..9e3d63289 --- /dev/null +++ b/tests/unit/requirements/test_profile_namespaces.py @@ -0,0 +1,230 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +import pytest +from pyshacl import validate +from rdflib import RDF, Graph, Literal, Namespace, URIRef +from rdflib.namespace import OWL, SH + +PROFILES_PATH = Path(__file__).parents[3] / "rocrate_validator/profiles" +PROFILE_NAMESPACE_PREFIX = "https://github.com/crs4/rocrate-validator/profiles/" +SCHEMA = Namespace("http://schema.org/") +EXAMPLE = Namespace("https://example.org/") + + +def _parse_graph(path: Path) -> Graph: + return Graph().parse(path, format="turtle") + + +def _profile_paths_with(*relative_paths: str) -> list[Path]: + return [ + ontology_path.parent + for ontology_path in sorted(PROFILES_PATH.rglob("ontology.ttl")) + if all((ontology_path.parent / relative_path).is_file() for relative_path in relative_paths) + ] + + +def _profile_id(profile_path: Path) -> str: + return str(profile_path.relative_to(PROFILES_PATH)) + + +def _collect_profile_namespaces(graph: Graph) -> set[str]: + """ + Return declared validator namespaces that occur in actual graph terms. + + Ignoring unused prefix declarations keeps the comparison focused on the + vocabulary IRIs that the artifact really uses. + """ + declared_namespaces = { + str(namespace) for _, namespace in graph.namespaces() if str(namespace).startswith(PROFILE_NAMESPACE_PREFIX) + } + terms = [str(term) for triple in graph for term in triple if isinstance(term, URIRef)] + return {namespace for namespace in declared_namespaces if any(term.startswith(namespace) for term in terms)} + + +def _profile_vocabulary_namespace(graph: Graph) -> str: + """Return the namespace explicitly bound as the artifact's own vocabulary.""" + try: + return str(next(namespace for prefix, namespace in graph.namespaces() if prefix == "ro-crate")) + except StopIteration as exc: + raise AssertionError("The graph must bind its profile vocabulary to the `ro-crate` prefix") from exc + + +def _parse_profile_artifacts(profile_path: Path, *, exclude: set[Path] | None = None) -> Graph: + graph = Graph() + excluded_paths = exclude or set() + for artifact_path in sorted(profile_path.rglob("*.ttl")): + if artifact_path not in excluded_paths: + graph.parse(artifact_path, format="turtle") + return graph + + +@pytest.mark.parametrize("profile_path", _profile_paths_with("ontology.ttl"), ids=_profile_id) +def test_profile_ontology_uses_the_namespace_of_its_shapes(profile_path: Path) -> None: + """ + The ontology's own profile vocabulary must also occur in its SHACL artifacts. + + This is the general invariant behind issue 193: if the ontology and shapes + use different namespaces, a derived profile can target an ontology class + that the base shapes never emit or target. External profile namespaces used + only by bridge axioms do not become part of the local vocabulary. + """ + ontology_path = profile_path / "ontology.ttl" + ontology = _parse_graph(ontology_path) + artifacts = _parse_profile_artifacts(profile_path, exclude={ontology_path}) + + ontology_namespace = _profile_vocabulary_namespace(ontology) + artifact_namespaces = _collect_profile_namespaces(artifacts) + + assert ontology_namespace in artifact_namespaces, ( + f"{ontology_path} uses the profile vocabulary {ontology_namespace}, but the profile's shapes do not" + ) + + +@pytest.mark.parametrize("profile_path", _profile_paths_with("ontology.ttl", "prefixes.ttl"), ids=_profile_id) +def test_sparql_prefixes_use_the_profile_ontology_namespace(profile_path: Path) -> None: + """ + The SPARQL prefix registry must use the ontology's vocabulary namespace. + + SPARQL constraints rely on this registry to expand prefixed class IRIs; a + different namespace would make those constraints refer to other resources. + """ + ontology = _parse_graph(profile_path / "ontology.ttl") + prefixes = _parse_graph(profile_path / "prefixes.ttl") + + ontology_namespace = _profile_vocabulary_namespace(ontology) + prefix_namespaces = _collect_profile_namespaces(prefixes) + + assert prefix_namespaces == {ontology_namespace}, ( + f"{profile_path} prefixes must use the ontology's own namespace: " + f"expected {ontology_namespace}, got {prefix_namespaces}" + ) + + +@pytest.mark.parametrize("marker", ["ROCrateMetadataFileDescriptor", "RootDataEntity"]) +def test_ro_crate_1_3_maps_the_inherited_marker(marker: str) -> None: + """The native 1.3 target class must match the marker emitted by inherited 1.2 discovery.""" + ontology = _parse_graph(PROFILES_PATH / "ro-crate/1.3/ontology.ttl") + ro_crate_1_2 = Namespace("https://github.com/crs4/rocrate-validator/profiles/ro-crate-1.2/") + ro_crate_1_3 = Namespace("https://github.com/crs4/rocrate-validator/profiles/ro-crate-1.3/") + + assert ( + ro_crate_1_3[marker], + OWL.equivalentClass, + ro_crate_1_2[marker], + ) in ontology + + +@pytest.mark.parametrize("source_version,target_version", [("1.2", "1.3"), ("1.3", "1.2")]) +@pytest.mark.parametrize("has_name", [False, True], ids=["missing-name", "with-name"]) +def test_root_data_entity_equivalence_applies_shacl_constraints( + source_version: str, target_version: str, has_name: bool +) -> None: + """Root constraints must apply across versions, rather than pass without focus nodes.""" + ontology = _parse_graph(PROFILES_PATH / "ro-crate/1.3/ontology.ttl") + source = Namespace(f"{PROFILE_NAMESPACE_PREFIX}ro-crate-{source_version}/") + target = Namespace(f"{PROFILE_NAMESPACE_PREFIX}ro-crate-{target_version}/") + + data = Graph() + data.add((EXAMPLE.root, RDF.type, source.RootDataEntity)) + if has_name: + data.add((EXAMPLE.root, SCHEMA.name, Literal("Example crate"))) + + shapes = Graph() + shapes.add((EXAMPLE.RootShape, RDF.type, SH.NodeShape)) + shapes.add((EXAMPLE.RootShape, SH.targetClass, target.RootDataEntity)) + shapes.add((EXAMPLE.RootShape, SH.property, EXAMPLE.RequiredName)) + shapes.add((EXAMPLE.RequiredName, SH.path, SCHEMA.name)) + shapes.add((EXAMPLE.RequiredName, SH.minCount, Literal(1))) + + conforms, report, _ = validate(data, shacl_graph=shapes, ont_graph=ontology, inference="owlrl") + + assert conforms == has_name + if not has_name: + results = list(report.subjects(RDF.type, SH.ValidationResult)) + assert len(results) == 1 + assert (results[0], SH.focusNode, EXAMPLE.root) in report + assert (results[0], SH.resultPath, SCHEMA.name) in report + + +@pytest.mark.parametrize( + "profile_path", + _profile_paths_with("ontology.ttl", "must/2_root_data_entity_metadata.ttl"), + ids=_profile_id, +) +def test_root_data_entity_iri_matches_between_ontology_and_shacl(profile_path: Path): + """ + The root-identification rule must emit the exact class IRI declared by the ontology. + + The assertion compares complete URI terms, not just the local name + ``RootDataEntity``. This catches a namespace mismatch at the point where + the base profile assigns the class to the root node. + """ + ontology = _parse_graph(profile_path / "ontology.ttl") + root_shapes = _parse_graph(profile_path / "must/2_root_data_entity_metadata.ttl") + + ontology_class = next( + node + for node in ontology.subjects(RDF.type, OWL.Class) + if isinstance(node, URIRef) and str(node).endswith("/RootDataEntity") + ) + + assert (None, SH.object, ontology_class) in root_shapes, ( + "SHACL root-identification rule must reference the same RootDataEntity IRI as the ontology" + ) + + +@pytest.mark.parametrize( + "profile_path", + _profile_paths_with("ontology.ttl", "must/2_root_data_entity_metadata.ttl"), + ids=_profile_id, +) +def test_external_shape_receives_focus_node_not_vacuous_conformance(profile_path: Path): + """ + An external shape targeting the ontology class must receive a focus node. + + The test adds a root node with the ontology-declared class and applies an + external shape requiring a missing ``schema:name``. With matching + namespaces, validation finds the node and fails; with the original mismatch, + it finds no focus node and incorrectly passes vacuously. + """ + ontology = _parse_graph(profile_path / "ontology.ttl") + root_class = next( + node + for node in ontology.subjects(RDF.type, OWL.Class) + if isinstance(node, URIRef) and str(node).endswith("/RootDataEntity") + ) + + data = Graph() + data.add((EXAMPLE.root, RDF.type, root_class)) + + external_shapes = Graph() + external_shapes.add((EXAMPLE.RootShape, RDF.type, SH.NodeShape)) + external_shapes.add((EXAMPLE.RootShape, SH.targetClass, root_class)) + external_shapes.add((EXAMPLE.RootShape, SH.property, EXAMPLE.RequiredName)) + external_shapes.add((EXAMPLE.RequiredName, SH.path, SCHEMA.name)) + external_shapes.add((EXAMPLE.RequiredName, SH.minCount, Literal(1))) + + conforms, _, _ = validate( + data, + shacl_graph=external_shapes, + ont_graph=ontology, + inference="owlrl", + ) + + assert not conforms, ( + "external shape must fail validation (missing required property), not pass vacuously due to namespace mismatch" + ) diff --git a/tests/unit/requirements/test_profiles.py b/tests/unit/requirements/test_profiles.py index 48b4c3469..f2260d609 100644 --- a/tests/unit/requirements/test_profiles.py +++ b/tests/unit/requirements/test_profiles.py @@ -13,17 +13,41 @@ # limitations under the License. import logging +from dataclasses import replace from pathlib import Path -from typing import Any +from shutil import copytree +from typing import Any, cast import pytest -from rdflib import Literal, Namespace +from rdflib import Graph, Literal, Namespace from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER, SHACL_NS from rocrate_validator.errors import DuplicateRequirementCheck, InvalidProfilePath, ProfileSpecificationError -from rocrate_validator.models import URI, Profile, ValidationContext, ValidationSettings, Validator +from rocrate_validator.events import EventType +from rocrate_validator.models import ( + URI, + CheckResult, + Profile, + RequirementCheckRelation, + Severity, + SkippedCheckDetail, + ValidationContext, + ValidationSettings, + Validator, +) +from rocrate_validator.models.events import RequirementCheckValidationEvent +from rocrate_validator.models.profile_check import ( + NoRequirementCheckOverrides, + ProfileCheckSuite, + RuleOverlayConsistency, +) from rocrate_validator.requirements.shacl.checks import SHACLCheck +from rocrate_validator.requirements.shacl.errors import SHACLValidationError from rocrate_validator.requirements.shacl.models import ShapesRegistry +from rocrate_validator.requirements.shacl.validator import ( + SHACLValidationAlreadyProcessed, + SHACLValidationContext, +) from tests.ro_crates import InvalidFileDescriptorEntity, ValidROC # set up logging @@ -71,8 +95,28 @@ def test_load_invalid_profile_from_validation_context(fake_profiles_path: str): # Check if the InvalidProfilePath exception is raised with pytest.raises(InvalidProfilePath): - profiles = context.profiles - logger.debug("The profiles: %r", profiles) + # Load the profiles + _ = context.profiles + + +def test_profile_detection_error_is_not_silenced(monkeypatch): + """Unexpected metadata errors must not become an empty profile selection.""" + + class BrokenMetadata: + def get_file_descriptor_entity(self): + raise RuntimeError("metadata profile detection failed") + + class BrokenROCrate: + metadata = BrokenMetadata() + + settings = ValidationSettings( + rocrate_uri=URI(ValidROC().wrroc_paper), + profile_identifier=DEFAULT_PROFILE_IDENTIFIER, + ) + monkeypatch.setattr(ValidationContext, "ro_crate", property(lambda self: BrokenROCrate())) + + with pytest.raises(RuntimeError, match="profile detection failed"): + Validator(settings).detect_rocrate_profiles() def test_load_valid_profile_without_inheritance_from_validation_context(fake_profiles_path: str): @@ -241,8 +285,7 @@ def test_load_invalid_profile_no_override_enabled(fake_profiles_path: str): with pytest.raises(DuplicateRequirementCheck): # Load the profiles - profiles = context.profiles - logger.debug("The profiles: %r", profiles) + _ = context.profiles def test_load_invalid_profile_with_override_on_same_profile(fake_profiles_path: str): @@ -264,8 +307,20 @@ def test_load_invalid_profile_with_override_on_same_profile(fake_profiles_path: with pytest.raises(DuplicateRequirementCheck): # Load the profiles - profiles = context.profiles - logger.debug("The profiles: %r", profiles) + _ = context.profiles + + +def test_validation_rejects_duplicate_check_identity_with_overrides_enabled(fake_profiles_path: str): + settings = ValidationSettings( + profiles_path=Path(fake_profiles_path), + profile_identifier="invalid-duplicated-shapes", + rocrate_uri=URI(ValidROC().wrroc_paper), + enable_profile_inheritance=True, + allow_requirement_check_override=True, + ) + + with pytest.raises(DuplicateRequirementCheck, match="Check Metadata File Descriptor entity existence"): + _ = ValidationContext(Validator(settings), settings).profiles def test_load_valid_profile_with_override_on_inherited_profile(fake_profiles_path: str): @@ -297,7 +352,468 @@ def test_load_valid_profile_with_override_on_inherited_profile(fake_profiles_pat assert len(requirements_checks) == 3, "The number of requirements should be 2" -def test_zero_shape_target_profile_triggers_pyshacl_run(fake_profiles_path: str): +def test_check_name_and_severity_are_unique_within_each_profile(): + profiles = Profile.load_profiles( + profiles_path=Path("rocrate_validator/profiles"), + severity=Severity.OPTIONAL, + ) + for profile in profiles: + checks = [check for requirement in profile.requirements for check in requirement.get_checks()] + identities = {(check.name, check.severity) for check in checks} + assert len(identities) == len(checks), profile.identifier + + +def test_profile_checks_return_structured_cached_results(): + profile = next( + item + for item in Profile.load_profiles(Path("rocrate_validator/profiles"), severity=Severity.OPTIONAL) + if item.identifier == "ro-crate-1.2" + ) + + first = profile.validate_checks() + second = profile.validate_checks() + + assert first is second + assert {result.check_id for result in first} == { + "unique-requirement-check-identity", + "rule-overlay-consistency", + } + assert all(result.passed for result in first) + + +def test_disabling_check_override_rejects_parent_match(check_overriding_profiles_path: str): + with pytest.raises(DuplicateRequirementCheck, match=r"\[REQUIRED\]"): + Profile.load_profiles( + check_overriding_profiles_path, + severity=Severity.OPTIONAL, + allow_requirement_check_override=False, + ) + + +def test_requirement_check_override_policy_is_not_a_default_profile_check(): + assert NoRequirementCheckOverrides not in ProfileCheckSuite.DEFAULT_CHECKS + + +def test_requirement_check_override_policy_returns_structured_failure(check_overriding_profiles_path: str): + profiles = Profile.load_profiles(check_overriding_profiles_path, severity=Severity.OPTIONAL) + overriding_profile = next(item for item in profiles if item.identifier == "b") + + result = NoRequirementCheckOverrides().run(overriding_profile) + + assert not result.passed + assert result.check_id == "no-requirement-check-overrides" + assert result.details == { + "name": "Check S", + "severity": "REQUIRED", + "sources": "a", + } + + +def test_rule_overlay_sources_are_consistent(check_overriding_profiles_path: str): + profiles = Profile.load_profiles(check_overriding_profiles_path, severity=Severity.OPTIONAL) + overlay = next(item for item in profiles if item.identifier == "b") + + result = RuleOverlayConsistency().run(overlay) + + assert result.passed + + +def test_rule_overlay_source_must_be_a_direct_parent(check_overriding_profiles_path: str, monkeypatch): + profiles = Profile.load_profiles(check_overriding_profiles_path, severity=Severity.OPTIONAL) + overlay = next(item for item in profiles if item.identifier == "d") + original_is_rule_overlay_of = cast("Any", Profile.is_rule_overlay_of).fget + assert original_is_rule_overlay_of is not None + monkeypatch.setattr( + Profile, + "is_rule_overlay_of", + property( + lambda profile: ( + ["https://w3id.org/a"] + if profile.identifier == overlay.identifier + else original_is_rule_overlay_of(profile) + ) + ), + ) + + result = RuleOverlayConsistency().run(overlay) + + assert not result.passed + assert result.message == "Rule overlay source is not a direct parent" + assert result.details == {"source": "https://w3id.org/a"} + + +def test_multiple_rule_overlay_sources_require_distinct_check_identities( + check_overriding_profiles_path: str, + monkeypatch, +): + profiles = Profile.load_profiles(check_overriding_profiles_path, severity=Severity.OPTIONAL) + overlay = next(item for item in profiles if item.identifier == "y") + original_is_rule_overlay_of = cast("Any", Profile.is_rule_overlay_of).fget + assert original_is_rule_overlay_of is not None + monkeypatch.setattr( + Profile, + "is_rule_overlay_of", + property( + lambda profile: ( + ["https://w3id.org/e", "https://w3id.org/f"] + if profile.identifier == overlay.identifier + else original_is_rule_overlay_of(profile) + ) + ), + ) + + result = RuleOverlayConsistency().run(overlay) + + assert not result.passed + assert result.message == "Rule overlay sources contain an ambiguous check identity" + assert result.details["name"] == "Check S" + assert result.details["severity"] == "REQUIRED" + + +def test_check_name_and_severity_match_parent_override(check_overriding_profiles_path: str): + profiles = Profile.load_profiles(check_overriding_profiles_path, severity=Severity.OPTIONAL) + parent = next(item for item in profiles if item.identifier == "a") + child = next(item for item in profiles if item.identifier == "b") + + parent_check = parent.get_requirement_check("Check the name of the entity", Severity.REQUIRED) + child_check = child.get_requirement_check("Check the name of the entity", Severity.REQUIRED) + + assert parent_check is not None + assert child_check is not None + assert child_check.overrides == [parent_check] + assert child_check in parent_check.overridden_by + + settings = ValidationSettings( + profiles_path=Path(check_overriding_profiles_path), + profile_identifier="b", + rocrate_uri=URI(ValidROC().wrroc_paper), + ) + context = ValidationContext(Validator(settings), settings) + original_order = child_check.order_number + child_check.order_number = 99 + try: + assert ( + context.effective_check_identifier(child_check) == f"b_{parent_check.relative_identifier.split(' ', 1)[-1]}" + ) + finally: + child_check.order_number = original_order + + +def test_effective_checks_expose_overlay_provenance(): + profiles = Profile.load_profiles("tests/data/profiles/effective_checks", severity=Severity.OPTIONAL) + profile = next(item for item in profiles if item.identifier == "effective-b") + + effective_checks = profile.get_effective_requirement_checks() + by_name = {item.check.name: item for item in effective_checks} + + assert len(effective_checks) == 2 + replacement = by_name["Shared check"] + assert replacement.relation == RequirementCheckRelation.REPLACES + assert replacement.identifier == "effective-b_1.2" + assert replacement.source_identifier == "effective-b_1.1" + assert [check.identifier for check in replacement.replaces] == ["effective-a_1.2"] + + inherited = by_name["Inherited check"] + assert inherited.relation == RequirementCheckRelation.INHERITED + assert inherited.profile == profile + assert inherited.identifier == "effective-b_1.1" + assert inherited.source_identifier == "effective-a_1.1" + assert inherited.source_profile.identifier == "effective-a" + assert inherited.replaces == () + assert inherited.to_dict() == { + "identifier": "effective-b_1.1", + "profile": "effective-b", + "source_identifier": "effective-a_1.1", + "source_profile": "effective-a", + "relation": "inherited", + "replaces": [], + } + + +def test_nested_overlay_preserves_effective_identity_and_orders_ancestors(): + """A wrapper profile keeps its inherited overlay's identity and execution order.""" + profiles = Profile.load_profiles("tests/data/profiles/effective_checks", severity=Severity.OPTIONAL) + profile = next(item for item in profiles if item.identifier == "effective-c") + + assert [item.identifier for item in profile.inherited_profiles] == ["effective-a", "effective-b"] + + inherited = next( + item for item in profile.get_effective_requirement_checks() if item.check.name == "Inherited check" + ) + assert inherited.identifier == "effective-b_1.1" + assert inherited.source_identifier == "effective-a_1.1" + assert inherited.profile.identifier == "effective-b" + assert inherited.source_profile.identifier == "effective-a" + + effective_checks = profile.get_effective_requirement_checks() + identifiers = [item.identifier for item in effective_checks] + assert len(identifiers) == len(set(identifiers)) + replacement = next(item for item in effective_checks if item.check.name == "Shared check") + assert replacement.identifier == "effective-b_1.2" + assert replacement.source_identifier == "effective-b_1.1" + assert replacement.relation == RequirementCheckRelation.INHERITED + + +@pytest.mark.parametrize("local_replacement", [False, True]) +def test_chained_overlays_rebase_effective_identifiers(tmp_path, local_replacement): + """A second overlay must rebase both inherited and replacement identities.""" + profiles_path = tmp_path / "profiles" + copytree("tests/data/profiles/effective_checks", profiles_path) + specification = profiles_path / "c/profile.ttl" + graph = Graph().parse(specification, format="turtle") + profiles_ns = Namespace("https://w3id.org/rocrate-validator/tests/effective-checks/") + validator_ns = Namespace("https://github.com/crs4/rocrate-validator/") + graph.add((profiles_ns.c, validator_ns.isRuleOverlayOf, profiles_ns.b)) + graph.serialize(specification, format="turtle") + if local_replacement: + (profiles_path / "c/checks.py").write_text( + (profiles_path / "b/checks.py").read_text(encoding="utf-8"), encoding="utf-8" + ) + + profiles = Profile.load_profiles(profiles_path, severity=Severity.OPTIONAL) + profile = next(item for item in profiles if item.identifier == "effective-c") + by_name = {item.check.name: item for item in profile.get_effective_requirement_checks()} + assert by_name["Inherited check"].identifier == "effective-c_1.1" + assert by_name["Inherited check"].source_identifier == "effective-a_1.1" + assert by_name["Shared check"].identifier == "effective-c_1.2" + assert by_name["Shared check"].source_identifier == ("effective-c_1.1" if local_replacement else "effective-b_1.1") + assert all(item.profile == profile for item in by_name.values()) + + +@pytest.mark.parametrize("explicit_ancestry", [False, True]) +def test_inherited_profiles_order_shared_ancestors(tmp_path, explicit_ancestry): + """Shared ancestors must precede descendants, even with redundant parent edges.""" + relations = { + "b": "", + "a": "prof:isProfileOf ;", + "c": "prof:isProfileOf , ;", + "d": "prof:isProfileOf ;", + } + if explicit_ancestry: + relations["d"] += " prof:isTransitiveProfileOf , , ;" + for name, predicates in relations.items(): + directory = tmp_path / name + directory.mkdir() + (directory / "profile.ttl").write_text( + "@prefix prof: .\n" + f' a prof:Profile ; {predicates} prof:hasToken "{name}" .', + encoding="utf-8", + ) + + profiles = Profile.load_profiles(tmp_path) + profile = next(item for item in profiles if item.identifier == "d") + assert [item.identifier for item in profile.inherited_profiles] == ["b", "a", "c"] + + +def test_effective_checks_retain_ordinary_inherited_identity(): + profiles = Profile.load_profiles(Path("rocrate_validator/profiles"), severity=Severity.OPTIONAL) + profile = next(item for item in profiles if item.identifier == "process-run-crate-0.5") + inherited = next( + item + for item in profile.get_effective_requirement_checks() + if item.relation == RequirementCheckRelation.INHERITED + ) + + assert inherited.profile == inherited.source_profile + assert inherited.identifier == inherited.source_identifier + assert inherited.source_profile != profile + + +def test_same_name_with_different_severity_does_not_override(): + profiles = Profile.load_profiles( + profiles_path=Path("rocrate_validator/profiles"), + severity=Severity.OPTIONAL, + allow_requirement_check_override=True, + ) + workflow_run = next(item for item in profiles if item.identifier == "workflow-run-crate-0.5") + process_run = next(item for item in profiles if item.identifier == "process-run-crate-0.5") + + required = workflow_run.get_requirement_check("Root Data Entity conformsTo", Severity.REQUIRED) + recommended = workflow_run.get_requirement_check("Root Data Entity conformsTo", Severity.RECOMMENDED) + parent = process_run.get_requirement_check("Root Data Entity conformsTo", Severity.REQUIRED) + + assert required is not None + assert recommended is not None + assert parent is not None + assert required.overrides == [parent] + assert recommended.overrides == [] + + +def test_rule_overlay_effective_identity_is_context_local(check_overriding_profiles_path: str): + settings = ValidationSettings( + profiles_path=Path(check_overriding_profiles_path), + profile_identifier="b", + rocrate_uri=URI(ValidROC().wrroc_paper), + ) + context = ValidationContext(Validator(settings), settings) + parent = next(item for item in context.profiles if item.identifier == "a") + inherited_check = parent.get_requirement_check("Check the name of the entity", Severity.REQUIRED) + + assert inherited_check is not None + source_identifier = inherited_check.identifier + assert context.is_rule_overlay_source(parent) + assert context.effective_check_profile(inherited_check).identifier == "b" + assert context.effective_check_identifier(inherited_check).startswith("b_") + assert inherited_check.identifier == source_identifier + assert inherited_check.requirement.profile.identifier == "a" + + issue = context.result.add_issue("test issue", inherited_check) + serialized_check = issue.to_dict()["check"] + assert serialized_check["identifier"].startswith("b_") + assert serialized_check["profile"] == "b" + assert serialized_check["source_identifier"] == source_identifier + assert serialized_check["source_profile"] == "a" + + # Keep internal outcomes for dependency resolution, but do not count a + # replaced source check among the reportable skips of the overlay. + context.result._record_check_result(inherited_check, CheckResult.SKIPPED, "test skip") + assert context.result.get_check_result(inherited_check) is CheckResult.SKIPPED + assert context.result.skipped_checks_count == 0 + assert context.result.skipped_check_details == [] + serialized_skip = SkippedCheckDetail(inherited_check, "test skip", context=context).to_dict() + assert serialized_skip["identifier"].startswith("b_") + assert serialized_skip["profile"] == "b" + assert serialized_skip["source_identifier"] == source_identifier + assert serialized_skip["source_profile"] == "a" + assert context.result.statistics.effective_check_identifier(inherited_check).startswith("b_") + assert context.result.statistics.effective_check_profile(inherited_check).identifier == "b" + + event = RequirementCheckValidationEvent(EventType.REQUIREMENT_CHECK_VALIDATION_END, inherited_check) + event.set_effective_identity( + context.effective_check_identifier(inherited_check), + context.effective_check_profile(inherited_check).identifier, + ) + assert event.effective_identifier.startswith("b_") + assert event.effective_profile_identifier == "b" + assert event.source_identifier == source_identifier + assert event.source_profile_identifier == "a" + + suppressed_settings = replace(settings, disable_inherited_profiles_issue_reporting=True) + suppressed_context = ValidationContext(Validator(suppressed_settings), suppressed_settings) + assert suppressed_context.result.statistics.total_checks == context.result.statistics.total_checks + + +def test_rule_overlay_keeps_source_before_target(check_overriding_profiles_path: str): + """Verify that overlay composition retains general-to-specific traversal.""" + settings = ValidationSettings( + profiles_path=Path(check_overriding_profiles_path), + profile_identifier="b", + rocrate_uri=URI(ValidROC().wrroc_paper), + enable_profile_inheritance=True, + allow_requirement_check_override=True, + ) + + context = ValidationContext(Validator(settings), settings) + + assert [profile.identifier for profile in context.profiles] == ["a", "b"] + + +def test_rule_overlay_check_can_be_skipped_by_source_or_effective_identifier( + check_overriding_profiles_path: str, +) -> None: + """Accept either check identity when configuring an overlay skip.""" + settings = ValidationSettings( + profiles_path=Path(check_overriding_profiles_path), + profile_identifier="b", + rocrate_uri=URI(ValidROC().wrroc_paper), + ) + context = ValidationContext(Validator(settings), settings) + source_profile = next(profile for profile in context.profiles if profile.identifier == "a") + source_check = source_profile.get_requirement_check("Check the name of the entity", Severity.REQUIRED) + + assert source_check is not None + supported_identifiers = ( + source_check.identifier, + context.effective_check_identifier(source_check), + ) + + for identifier in supported_identifiers: + skip_settings = replace(settings, skip_checks=[identifier]) + skip_context = ValidationContext(Validator(skip_settings), skip_settings) + skip_source = next(profile for profile in skip_context.profiles if profile.identifier == "a") + skip_check = skip_source.get_requirement_check("Check the name of the entity", Severity.REQUIRED) + + assert skip_check is not None + assert skip_context.is_check_skipped(skip_check) + + +def test_shacl_overlay_profiles_are_loaded_once_before_processing( + check_overriding_profiles_path: str, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Add each overlay profile's shapes once while allowing deferred re-entry.""" + settings = ValidationSettings( + profiles_path=Path(check_overriding_profiles_path), + profile_identifier="b", + rocrate_uri=URI(ValidROC().wrroc_paper), + enable_profile_inheritance=True, + allow_requirement_check_override=True, + ) + context = ValidationContext(Validator(settings), settings) + source = next(profile for profile in context.profiles if profile.identifier == "a") + target = next(profile for profile in context.profiles if profile.identifier == "b") + shacl_context = SHACLValidationContext.get_instance(context) + shape_registry_loads = 0 + extend_shapes = shacl_context.shapes_registry.extend + + def record_shape_load(shapes: dict[str, Any], graph: Graph) -> None: + """Count each profile contribution to the combined SHACL graph.""" + nonlocal shape_registry_loads + shape_registry_loads += 1 + extend_shapes(shapes, graph) + + monkeypatch.setattr(shacl_context.shapes_registry, "extend", record_shape_load) + + assert shacl_context.__set_current_validation_profile__(source) + assert shacl_context.__set_current_validation_profile__(source) + assert shacl_context.__set_current_validation_profile__(target) + assert shape_registry_loads == 2 + + shacl_context.current_validation_result = True + with pytest.raises(SHACLValidationAlreadyProcessed): + shacl_context.__set_current_validation_profile__(target) + + +def test_normally_inherited_check_keeps_source_identity(check_overriding_profiles_path: str): + settings = ValidationSettings( + profiles_path=Path(check_overriding_profiles_path), + profile_identifier="c", + rocrate_uri=URI(ValidROC().wrroc_paper), + ) + context = ValidationContext(Validator(settings), settings) + parent = next(item for item in context.profiles if item.identifier == "a") + inherited_check = parent.get_requirement_check("Check the name of the entity", Severity.REQUIRED) + + assert inherited_check is not None + assert not context.is_rule_overlay_source(parent) + assert context.effective_check_profile(inherited_check).identifier == "a" + assert context.effective_check_identifier(inherited_check) == inherited_check.identifier + + +def test_overlay_identity_is_isolated_between_consecutive_contexts(check_overriding_profiles_path: str): + base_settings = ValidationSettings( + profiles_path=Path(check_overriding_profiles_path), + profile_identifier="b", + rocrate_uri=URI(ValidROC().wrroc_paper), + ) + overlay_context = ValidationContext(Validator(base_settings), base_settings) + overlay_parent = next(item for item in overlay_context.profiles if item.identifier == "a") + overlay_check = overlay_parent.get_requirement_check("Check the name of the entity", Severity.REQUIRED) + assert overlay_check is not None + assert overlay_context.effective_check_profile(overlay_check).identifier == "b" + + inherited_settings = replace(base_settings, profile_identifier="c") + inherited_context = ValidationContext(Validator(inherited_settings), inherited_settings) + inherited_parent = next(item for item in inherited_context.profiles if item.identifier == "a") + inherited_check = inherited_parent.get_requirement_check("Check the name of the entity", Severity.REQUIRED) + assert inherited_check is not None + assert inherited_context.effective_check_profile(inherited_check).identifier == "a" + assert inherited_context.effective_check_identifier(inherited_check) == inherited_check.identifier + + assert overlay_context.effective_check_profile(overlay_check).identifier == "b" + + +def test_zero_shape_target_profile_triggers_pyshacl_run(monkeypatch, fake_profiles_path: str): """Regression test for the 0-shape profile bug: when the target profile has no SHACL checks of its own, Validator must still drive a single pyshacl run @@ -305,6 +821,7 @@ def test_zero_shape_target_profile_triggers_pyshacl_run(fake_profiles_path: str) Without the fix in `Validator.__ensure_target_shacl_run__`, no SHACLCheck would be recorded as executed for the wrapper target.""" + monkeypatch.setattr(ValidationContext, "data_graph", property(lambda self: Graph())) settings = ValidationSettings( profiles_path=Path(fake_profiles_path), profile_identifier="c-wrapper", @@ -323,6 +840,52 @@ def test_zero_shape_target_profile_triggers_pyshacl_run(fake_profiles_path: str) ) +def test_pyshacl_engine_failure_is_not_silenced(monkeypatch, fake_profiles_path: str): + """A pySHACL crash must abort validation instead of producing a clean result.""" + + def fail_validation(*args, **kwargs): + raise ImportError("cannot import name 'ConjunctiveLike' from 'pyshacl.consts'") + + monkeypatch.setattr(ValidationContext, "data_graph", property(lambda self: Graph())) + monkeypatch.setattr("rocrate_validator.requirements.shacl.validator.pyshacl.validate", fail_validation) + settings = ValidationSettings( + profiles_path=Path(fake_profiles_path), + profile_identifier="c-deactivated", + rocrate_uri=URI(ValidROC().wrroc_paper), + enable_profile_inheritance=True, + allow_requirement_check_override=True, + disable_check_for_duplicates=True, + ) + + with pytest.raises(SHACLValidationError, match="ConjunctiveLike") as exc_info: + Validator(settings).validate() + + assert isinstance(exc_info.value.__cause__, ImportError) + + +def test_pyshacl_engine_failure_in_zero_shape_finalizer_is_not_silenced(monkeypatch, fake_profiles_path: str): + """The forced SHACL run for an inheritance-only target must also fail closed.""" + + def fail_validation(*args, **kwargs): + raise ImportError("cannot import name 'ConjunctiveLike' from 'pyshacl.consts'") + + monkeypatch.setattr(ValidationContext, "data_graph", property(lambda self: Graph())) + monkeypatch.setattr("rocrate_validator.requirements.shacl.validator.pyshacl.validate", fail_validation) + settings = ValidationSettings( + profiles_path=Path(fake_profiles_path), + profile_identifier="c-wrapper", + rocrate_uri=URI(ValidROC().wrroc_paper), + enable_profile_inheritance=True, + allow_requirement_check_override=True, + disable_check_for_duplicates=True, + ) + + with pytest.raises(SHACLValidationError, match="ConjunctiveLike") as exc_info: + Validator(settings).validate() + + assert isinstance(exc_info.value.__cause__, ImportError) + + def test_profile_parents(check_overriding_profiles_path: str): """Test the order of the loaded profiles.""" logger.debug("The profiles path: %r", check_overriding_profiles_path) @@ -477,6 +1040,33 @@ def enabled(self, ctx): assert enabled.deactivated is False # pyright: ignore[reportFunctionMemberAccess] +def test_python_check_decorator_sets_dependencies(): + """The @check decorator must preserve declared check dependencies.""" + from rocrate_validator.requirements.python import check + + @check(name="dependent", depends_on=("base",)) + def dependent(self, ctx): + return True + + assert dependent.depends_on == ("base",) # pyright: ignore[reportFunctionMemberAccess] + + +def test_python_check_decorator_sets_extra_check_flag(): + """The @check decorator must expose the extra_check classification.""" + from rocrate_validator.requirements.python import check + + @check(name="advisory", extra_check=True) + def advisory(self, ctx): + return True + + @check(name="conformance") + def conformance(self, ctx): + return True + + assert advisory.extra_check is True # pyright: ignore[reportFunctionMemberAccess] + assert conformance.extra_check is False # pyright: ignore[reportFunctionMemberAccess] + + def test_shacl_shape_with_deactivated_marks_check_skipped(fake_profiles_path: str): """A child profile that overrides an inherited NodeShape by `sh:name` and sets `sh:deactivated true` should produce a check whose `deactivated` @@ -588,7 +1178,8 @@ def test_shacl_check_deactivation_scoped_to_descendants(fake_profiles_path: str) # Trigger lazy loading. for p in all_profiles: - _ = p.requirements + if p.token != "invalid-duplicated-shapes": + _ = p.requirements _ = context.profiles # warm context too parent_shape_check = next(c for c in parent_c.requirements[0].get_checks() if isinstance(c, SHACLCheck)) diff --git a/tests/unit/requirements/test_shacl_utils.py b/tests/unit/requirements/test_shacl_utils.py index 63b51dde2..18cc97dcf 100644 --- a/tests/unit/requirements/test_shacl_utils.py +++ b/tests/unit/requirements/test_shacl_utils.py @@ -27,7 +27,9 @@ from rdflib.collection import Collection from rocrate_validator.constants import SHACL_NS -from rocrate_validator.requirements.shacl.utils import load_shapes_from_graph +from rocrate_validator.errors import BadSyntaxError +from rocrate_validator.requirements.shacl.utils import load_shapes_from_file, load_shapes_from_graph +from rocrate_validator.utils.rdf import rebase_graph SH = Namespace(SHACL_NS) EX = Namespace("http://example.org/") @@ -65,6 +67,51 @@ def _build_two_property_shape() -> tuple[Graph, URIRef, BNode, BNode]: return g, node_shape, prop_a, prop_b +@pytest.mark.parametrize("directive", ["@prefix", "PREFIX"]) +def test_turtle_rebasing_only_changes_lexically_relative_iris(tmp_path, directive): + source_base = "https://github.com/crs4/rocrate-validator/prepared/crate/" + target_base = "file:///tmp/crate/" + absolute_target = URIRef("https://github.com/crs4/rocrate-validator/prepared/crate/AbsoluteClass") + external_target = URIRef("https://example.invalid/external/AbsoluteClass") + shapes_path = tmp_path / "relative-and-absolute.ttl" + relative_prefix = "@prefix rel: <../> ." if directive == "@prefix" else "PREFIX rel: <../>" + shapes_path.write_text( + f""" + @prefix sh: . + {relative_prefix} + PREFIX abs: + + rel:Shape a sh:NodeShape ; sh:targetNode rel:target . + abs:AbsoluteShape a sh:NodeShape ; sh:targetNode abs:AbsoluteClass . + + a sh:NodeShape ; sh:targetNode . + """, + encoding="utf-8", + ) + + shapes = load_shapes_from_file(str(shapes_path), source_base) + rebased = rebase_graph( + shapes.shapes_graph, + ((source_base, target_base),), + rebase_nodes=shapes.relative_iris, + ) + + assert any(object_ == URIRef("file:///tmp/target") for _, _, object_ in rebased) + assert any(object_ == absolute_target for _, _, object_ in rebased) + assert any(object_ == external_target for _, _, object_ in rebased) + + +def test_turtle_loader_accepts_utf8_bom(tmp_path): + shapes_path = tmp_path / "bom.ttl" + shapes_path.write_bytes( + b"\xef\xbb\xbf@prefix sh: . a sh:NodeShape ." + ) + + shapes = load_shapes_from_file(str(shapes_path)) + + assert URIRef("https://example.org/Shape") in shapes.node_shapes + + def test_returns_link_triple_to_target_property(): """The link ``(node_shape, sh:property, shape_property)`` must be present.""" g, node_shape, prop_a, prop_b = _build_two_property_shape() @@ -197,3 +244,17 @@ def test_unknown_shape_node_raises(): with pytest.raises(KeyError): shapes_list.get_shape_property_graph(EX.UnknownShape, prop_a) + + +def test_load_shapes_from_file_reports_parser_location(tmp_path): + """Parser errors preserve the line and character of the invalid Turtle.""" + shapes_path = tmp_path / "broken.ttl" + shapes_path.write_text("@prefix ex: .\n xxxx\n", encoding="utf-8") + + with pytest.raises(BadSyntaxError) as exc_info: + load_shapes_from_file(str(shapes_path)) + + error = exc_info.value + assert error.path == str(shapes_path) + assert error.line == 2 + assert error.character == 5 diff --git a/tests/unit/test_check_result.py b/tests/unit/test_check_result.py new file mode 100644 index 000000000..bdd8f8fa8 --- /dev/null +++ b/tests/unit/test_check_result.py @@ -0,0 +1,58 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any, cast # pylint: disable=unused-import + +import pytest + +from rocrate_validator.models import CheckResult, normalize_check_result +from rocrate_validator.requirements.python import check + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (True, CheckResult.PASSED), + (False, CheckResult.FAILED), + (None, CheckResult.SKIPPED), + (CheckResult.PASSED, CheckResult.PASSED), + ], +) +def test_normalize_check_result(value, expected): + assert normalize_check_result(value) is expected + + +def test_normalize_check_result_rejects_invalid_values(): + with pytest.raises(TypeError, match="Check results must be bool"): + normalize_check_result(cast("Any", "passed")) + + +@pytest.mark.parametrize("result_type", [bool, CheckResult, type(None), bool | CheckResult | None]) +def test_check_decorator_accepts_tri_state_return_annotations(result_type): + def check_function(check_instance, context): + return True + + check_function.__annotations__["return"] = result_type + decorated = check()(check_function) + + check_attribute = "check" + assert getattr(decorated, check_attribute) + + +def test_check_decorator_rejects_invalid_return_annotation(): + with pytest.raises(RuntimeError, match="return bool, CheckResult, or None"): + + @check() + def invalid_check(check_instance, context) -> int: + return 1 diff --git a/tests/unit/test_default_profile.py b/tests/unit/test_default_profile.py new file mode 100644 index 000000000..a6907835d --- /dev/null +++ b/tests/unit/test_default_profile.py @@ -0,0 +1,140 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Tests for the default validation profile and for the resolution of bare +profile tokens (e.g. `ro-crate`) to a concrete profile version. +""" + +import logging +from http import HTTPStatus +from types import SimpleNamespace + +import pytest +from rdflib import Graph + +from rocrate_validator.constants import DEFAULT_PROFILE_IDENTIFIER +from rocrate_validator.models import URI, Profile, Severity, ValidationContext, ValidationSettings +from rocrate_validator.services import get_profile, get_profiles, validate +from rocrate_validator.utils.http import HttpRequester +from tests.ro_crates import ValidROC +from tests.ro_crates_v1_3 import ValidROCrate13 +from tests.shared import RO_CRATE_1_1_PROFILE_IDENTIFIER + +logger = logging.getLogger(__name__) + +EXPECTED_DEFAULT_PROFILE_IDENTIFIER = "ro-crate-1.3" + + +@pytest.fixture(autouse=True) +def local_jsonld_context(monkeypatch: pytest.MonkeyPatch) -> None: + """Keep default-profile unit tests independent from remote contexts.""" + response = SimpleNamespace( + status_code=HTTPStatus.OK, + headers={"Content-Type": "application/ld+json"}, + json=lambda: {"@context": {}}, + ) + monkeypatch.setattr(HttpRequester, "get", lambda self, url, **kwargs: response, raising=False) + monkeypatch.setattr(ValidationContext, "data_graph", property(lambda self: Graph())) + + +def test_default_profile_identifier_is_ro_crate_1_3(): + """The base RO-Crate profile used by default is 1.3.""" + assert DEFAULT_PROFILE_IDENTIFIER == EXPECTED_DEFAULT_PROFILE_IDENTIFIER + + +def test_validation_settings_default_profile(): + """`ValidationSettings` picks up the default profile when none is given.""" + settings = ValidationSettings(rocrate_uri=URI(str(ValidROC().wrroc_paper))) + assert settings.profile_identifier == EXPECTED_DEFAULT_PROFILE_IDENTIFIER + + +def test_bare_token_resolves_to_highest_version_via_services(): + """`get_profile` resolves a bare token to the highest available version.""" + assert get_profile("ro-crate").identifier == EXPECTED_DEFAULT_PROFILE_IDENTIFIER + + +def test_explicit_identifier_wins_over_token_resolution(): + """An exact identifier is never overridden by the highest-version rule.""" + assert get_profile(RO_CRATE_1_1_PROFILE_IDENTIFIER).identifier == RO_CRATE_1_1_PROFILE_IDENTIFIER + assert get_profile("ro-crate-1.3").identifier == "ro-crate-1.3" + + +def test_bare_token_resolves_to_highest_version_in_validation_context(): + """ + The validation context resolves a bare token the same way `get_profile` does. + + These two used to disagree: the context picked the highest version while + `Profile.find_in_list` returned the first match in load order. + """ + settings = ValidationSettings( + rocrate_uri=URI(str(ValidROC().wrroc_paper)), + profile_identifier="ro-crate", + requirement_severity=Severity.REQUIRED, + ) + validate(settings) + # The resolved identifier is written back to the settings + assert settings.profile_identifier == EXPECTED_DEFAULT_PROFILE_IDENTIFIER + + +def test_version_sort_key_orders_versions_numerically(): + """Versions are compared component-wise, so 1.10 sorts after 1.9.""" + profiles = get_profiles() + ro_crate_profiles = [p for p in profiles if p.token == "ro-crate"] + assert len(ro_crate_profiles) >= 2, "Expected at least two versions of the RO-Crate profile" + highest = max(ro_crate_profiles, key=lambda p: Profile.version_sort_key(p.version)) + assert highest.identifier == EXPECTED_DEFAULT_PROFILE_IDENTIFIER + # A plain string comparison would rank "1.9" above "1.10" + assert Profile.version_sort_key("1.10") > Profile.version_sort_key("1.9") + assert Profile.version_sort_key(None) < Profile.version_sort_key("0.1") + + +def test_default_profile_validates_a_1_3_crate() -> None: + """A 1.3 crate validates against the default profile without pinning it.""" + settings = ValidationSettings( + rocrate_uri=URI(str(ValidROCrate13().attached)), + requirement_severity=Severity.REQUIRED, + skip_checks=["ro-crate-1.3_4.1"], + ) + result = validate(settings) + assert result.passed(), "A valid RO-Crate 1.3 should pass under the default profile" + + +def test_default_profile_rejects_a_1_1_context() -> None: + """ + A 1.1-era crate no longer passes under the default profile. + + This documents the breaking part of the default switch: the crate declares the + 1.1 JSON-LD context, which 1.3 requires to be the 1.3 one. + """ + settings = ValidationSettings( + rocrate_uri=URI(str(ValidROC().wrroc_paper)), + requirement_severity=Severity.REQUIRED, + skip_checks=["ro-crate-1.3_4.1"], + ) + result = validate(settings) + assert not result.passed() + failed_check_ids = {issue.to_dict()["check"]["identifier"] for issue in result.get_issues()} + assert "ro-crate-1.3_4.2" in failed_check_ids, ( + f"Expected the 1.3 context check to fail, got: {sorted(failed_check_ids)}" + ) + + # ...and the same context is accepted when explicitly pinned to 1.1. This + # assertion deliberately ignores unrelated SHACL results because this unit + # test replaces the data graph to avoid resolving remote JSON-LD contexts. + settings.profile_identifier = RO_CRATE_1_1_PROFILE_IDENTIFIER + settings.skip_checks = ["ro-crate-1.1_4.1"] + result = validate(settings) + failed_check_ids = {issue.to_dict()["check"]["identifier"] for issue in result.get_issues()} + assert "ro-crate-1.1_4.2" not in failed_check_ids diff --git a/tests/unit/test_dependency_propagation.py b/tests/unit/test_dependency_propagation.py new file mode 100644 index 000000000..5b69ca399 --- /dev/null +++ b/tests/unit/test_dependency_propagation.py @@ -0,0 +1,211 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from types import SimpleNamespace + +from rocrate_validator.models import CheckResult, Requirement, RequirementCheck, RequirementLoader, SkipCategory +from rocrate_validator.models.check_result import normalize_check_result + + +class _Profile: + identifier = "test" + + def __init__(self): + self.requirements = [] + + @property + def parents(self): + return [] + + @property + def siblings(self): + return [] + + def get_requirement_check(self, name): + for requirement in self.requirements: + check = requirement.get_check(name) + if check: + return check + return None + + +class _Requirement(Requirement): + def __init__(self, profile, name): + super().__init__(profile, name=name, initialize_checks=False) + profile.requirements.append(self) + + @property + def hidden(self): + return False + + def __init_checks__(self): + return [] + + +class _Check(RequirementCheck): + def __init__(self, requirement, name, result, depends_on=()): + super().__init__(requirement, name, depends_on=depends_on) + self.result = result + self.calls = 0 + + def execute_check(self, context): + self.calls += 1 + return self.result + + +class _Result: + def __init__(self): + self._check_results = {} + self.skipped_checks = set() + + def _record_check_result( + self, + check, + result, + skip_message=None, + skip_category=SkipCategory.RETURNED, + ): + normalized_result = normalize_check_result(result) + self._check_results[check.identifier] = normalized_result + if normalized_result is CheckResult.SKIPPED: + self.skipped_checks.add(check) + else: + self.skipped_checks.discard(check) + return normalized_result + + def _add_executed_check(self, check, result): + self._record_check_result(check, result) + + def _add_skipped_check(self, check): + self._record_check_result(check, CheckResult.SKIPPED) + + def get_check_result(self, check): + return self._check_results.get(check.identifier) + + +class _Validator: + def __init__(self): + self.events = [] + + def notify(self, event): + self.events.append(event) + + +class _Context: + def __init__(self, profile, skip_checks=(), fail_fast=False): + self.profile_identifier = profile.identifier + self.settings = SimpleNamespace( + skip_checks=list(skip_checks), + disable_inherited_profiles_issue_reporting=False, + ) + self.result = _Result() + self.validator = _Validator() + self.fail_fast = fail_fast + self.aborted = False + + @staticmethod + def resolve_dependency_check(check: RequirementCheck, dependency_name: str) -> RequirementCheck: + """Resolve dependencies using the minimal profile API exercised here.""" + dependency = check.requirement.profile.get_requirement_check(dependency_name) + assert dependency is not None + return dependency + + def is_check_skipped(self, check: RequirementCheck) -> bool: + """Return whether the test settings explicitly skip ``check``.""" + return check.identifier in self.settings.skip_checks + + +def _check(requirement, name, result, depends_on=(), deactivated=False): + check = _Check(requirement, name, result, depends_on=depends_on) + check._deactivated = deactivated + requirement._checks.append(check) + check.order_number = len(requirement._checks) + return check + + +def _run(requirements, *, skip_checks=(), fail_fast=False): + profile = requirements[0].profile + ordered_requirements = RequirementLoader.order_by_dependencies(requirements) + for order_number, requirement in enumerate(ordered_requirements, start=1): + requirement._order_number = order_number + context = _Context(profile, skip_checks=skip_checks, fail_fast=fail_fast) + for requirement in ordered_requirements: + requirement._do_validate_(context) + return context + + +def test_failed_dependency_skips_dependent_check(): + profile = _Profile() + base_requirement = _Requirement(profile, "Base") + dependent_requirement = _Requirement(profile, "Dependent") + base = _check(base_requirement, "base", False) + dependent = _check(dependent_requirement, "dependent", True, depends_on=("base",)) + + context = _run([dependent_requirement, base_requirement]) + + assert context.result.get_check_result(base) is CheckResult.FAILED + assert context.result.get_check_result(dependent) is CheckResult.SKIPPED + assert dependent.calls == 0 + + +def test_skipped_dependency_skips_dependent_check(): + profile = _Profile() + base_requirement = _Requirement(profile, "Base") + dependent_requirement = _Requirement(profile, "Dependent") + base = _check(base_requirement, "base", CheckResult.SKIPPED) + dependent = _check(dependent_requirement, "dependent", True, depends_on=("base",)) + + context = _run([base_requirement, dependent_requirement]) + + assert context.result.get_check_result(base) is CheckResult.SKIPPED + assert context.result.get_check_result(dependent) is CheckResult.SKIPPED + assert dependent.calls == 0 + + +def test_dependency_skip_propagates_transitively(): + profile = _Profile() + base_requirement = _Requirement(profile, "Base") + middle_requirement = _Requirement(profile, "Middle") + leaf_requirement = _Requirement(profile, "Leaf") + base = _check(base_requirement, "base", False) + middle = _check(middle_requirement, "middle", True, depends_on=("base",)) + leaf = _check(leaf_requirement, "leaf", True, depends_on=("middle",)) + + context = _run([leaf_requirement, middle_requirement, base_requirement]) + + assert context.result.get_check_result(base) is CheckResult.FAILED + assert context.result.get_check_result(middle) is CheckResult.SKIPPED + assert context.result.get_check_result(leaf) is CheckResult.SKIPPED + assert middle.calls == 0 + assert leaf.calls == 0 + + +def test_independent_checks_continue_after_dependency_failure(): + profile = _Profile() + base_requirement = _Requirement(profile, "Base") + dependent_requirement = _Requirement(profile, "Dependent") + independent_requirement = _Requirement(profile, "Independent") + base = _check(base_requirement, "base", False) + dependent = _check(dependent_requirement, "dependent", True, depends_on=("base",)) + independent = _check(independent_requirement, "independent", True) + + context = _run( + [dependent_requirement, independent_requirement, base_requirement], + fail_fast=False, + ) + + assert context.result.get_check_result(base) is CheckResult.FAILED + assert context.result.get_check_result(dependent) is CheckResult.SKIPPED + assert context.result.get_check_result(independent) is CheckResult.PASSED + assert independent.calls == 1 diff --git a/tests/unit/test_jsonld.py b/tests/unit/test_jsonld.py new file mode 100644 index 000000000..65ae696e2 --- /dev/null +++ b/tests/unit/test_jsonld.py @@ -0,0 +1,49 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rocrate_validator.utils.jsonld import find_singleton_property_arrays + + +def test_find_singleton_property_arrays_preserves_jsonld_semantics(): + metadata = { + "@context": [ + { + "setProperty": {"@id": "https://example.org/setProperty", "@container": "@set"}, + "listProperty": {"@id": "https://example.org/listProperty", "@container": "@list"}, + } + ], + "@graph": [ + { + "@id": "./", + "@type": ["Dataset", "Thing"], + "singletonReference": [{"@id": "file.txt"}], + "singletonLiteral": ["one"], + "multipleValues": ["one", "two"], + "emptyValues": [], + "setProperty": ["one"], + "listProperty": ["one"], + }, + {"@id": "file.txt", "name": [{"@value": "File"}]}, + ], + } + + assert find_singleton_property_arrays(metadata) == [ + ("./", "singletonReference"), + ("./", "singletonLiteral"), + ("file.txt", "name"), + ] + + +def test_find_singleton_property_arrays_ignores_invalid_graph_shape(): + assert not find_singleton_property_arrays({"@context": [], "@graph": {}}) diff --git a/tests/unit/test_jsonld_compact_iris.py b/tests/unit/test_jsonld_compact_iris.py new file mode 100644 index 000000000..83ff40f0a --- /dev/null +++ b/tests/unit/test_jsonld_compact_iris.py @@ -0,0 +1,159 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Regression coverage for the RO-Crate compact property-name policy.""" + +# Empty dictionaries are part of the helper contract; None must not pass. +# pylint: disable=use-implicit-booleaness-not-comparison + +import json +from copy import deepcopy +from unittest.mock import Mock + +import pytest +from rdflib import Graph +from rdflib.compare import isomorphic + +from rocrate_validator.utils.http import OfflineCacheMissError +from rocrate_validator.utils.jsonld import find_unexpected_compaction_keys, resolve_compaction_context + + +@pytest.mark.parametrize( + ("definition", "version", "accepted"), + [ + ("https://example.org/", 1.0, True), + ("https://example.org/ns", 1.0, True), + ("https://example.org#", 1.0, True), + ({"@id": "https://example.org/"}, 1.0, True), + ("https://example.org/", 1.1, True), + ({"@id": "https://example.org/", "@prefix": True}, 1.1, True), + ({"@id": "https://example.org/", "@prefix": False}, 1.1, False), + ({"@id": "https://example.org/"}, 1.1, False), + ({"@id": "https://example.org/ns", "@prefix": True}, 1.1, True), + ("https://example.org/name", 1.1, False), + (None, 1.0, False), + ({"@id": None}, 1.0, False), + ("@id", 1.0, False), + ("relative/", 1.0, False), + ], +) +def test_prefix_eligibility(definition, version, accepted): + context = {"@version": version, "ex": definition} + unexpected = find_unexpected_compaction_keys({"@id": "./", "ex:property": "value"}, context) + assert unexpected == ({} if accepted else {"ex:property": 1}) + + +def test_context_order_nullification_and_remote_loader(): + loader = Mock(return_value={"ex": "https://example.org/", "name": "http://schema.org/name"}) + context = ["https://example.org/context", {"ex": None}] + before = deepcopy(context) + definitions = resolve_compaction_context(context, loader) + assert find_unexpected_compaction_keys({"ex:property": 1, "name": "test"}, definitions) == {"ex:property": 1} + loader.assert_called_once_with("https://example.org/context") + assert context == before + assert resolve_compaction_context([{"ex": "https://example.org/"}, None], loader) == {} + + +def test_remote_cycles_fail(): + loader = Mock(return_value="https://example.org/context") + with pytest.raises(ValueError, match="Recursive JSON-LD context"): + resolve_compaction_context("https://example.org/context", loader) + + +def test_remote_cache_miss_propagates(): + loader = Mock(side_effect=OfflineCacheMissError("https://example.org/context")) + with pytest.raises(OfflineCacheMissError): + resolve_compaction_context("https://example.org/context", loader) + + +def test_compact_namespace_definition(): + definitions = {"base": "https://example.org/", "ex": "base:terms#"} + assert find_unexpected_compaction_keys({"ex:property": "value"}, definitions) == {} + + +def test_counts_and_non_node_payloads(): + entity = [ + {"@id": "./", "ex:property": [{"@value": "value", "@language": "en"}], "unknown": 1}, + {"@id": "#node", "unknown": 2, "@context": {"ex": "https://example.org/"}}, + ] + assert find_unexpected_compaction_keys(entity, {"ex": "https://example.org/"}) == {"unknown": 2} + + +def test_explicit_terms_and_absolute_iri_policy(): + definitions = {"ex:property": "https://example.org/property", "ex": "https://example.org/"} + assert find_unexpected_compaction_keys({"ex:property": 1}, definitions) == {} + assert find_unexpected_compaction_keys({"https://example.org/property": 1}, definitions) == { + "https://example.org/property": 1 + } + + +def test_prefix_and_explicit_term_preserve_rdf(): + prefixed = {"@context": {"ex": "https://example.org/"}, "@id": "https://example.org/node", "ex:property": "value"} + explicit = {"@context": {"property": "https://example.org/property"}, "@id": prefixed["@id"], "property": "value"} + assert isomorphic( + Graph().parse(data=json.dumps(prefixed), format="json-ld"), + Graph().parse(data=json.dumps(explicit), format="json-ld"), + ) + + +@pytest.mark.parametrize("disabled", [None, {"@id": None}]) +def test_explicitly_disabled_compact_key_does_not_fall_back_to_prefix(disabled): + definitions = {"ex": "https://example.org/", "ex:property": disabled} + assert find_unexpected_compaction_keys({"ex:property": 1}, definitions) == {"ex:property": 1} + + +def test_remote_context_reset_clears_previous_definitions(): + loader = Mock(return_value=[None, {"new": "https://example.org/"}]) + definitions = resolve_compaction_context([{"old": "https://example.org/"}, "https://example.org/context"], loader) + assert definitions == {"new": "https://example.org/"} + + +def test_later_namespace_override_does_not_rewrite_previous_mapping(): + definitions = resolve_compaction_context( + [ + {"base": "https://example.org/", "ex": "base:terms#"}, + {"base": "https://other.example/"}, + ], + Mock(), + ) + assert definitions["ex"] == "https://example.org/terms#" + assert definitions["base"] == "https://other.example/" + + +def test_remote_failure_is_not_ignored(): + loader = Mock(side_effect=RuntimeError("Unable to retrieve context")) + with pytest.raises(RuntimeError, match="Unable to retrieve"): + resolve_compaction_context("https://example.org/context", loader) + + +def test_relative_remote_reference_uses_parent_context_url(): + loader = Mock(side_effect=["nested.jsonld", {"ex": "https://example.org/"}]) + definitions = resolve_compaction_context("https://example.org/context/index.jsonld", loader) + assert loader.call_args_list[-1].args == ("https://example.org/context/nested.jsonld",) + assert find_unexpected_compaction_keys({"ex:property": 1}, definitions) == {} + + +@pytest.mark.parametrize( + "definitions", + [ + {"base": "https://example.org/", "ex": "base"}, + {"base": "https://example.org/", "ex": {"@id": "base"}}, + ], +) +def test_namespace_alias(definitions): + assert find_unexpected_compaction_keys({"ex:property": 1}, definitions) == {} + + +def test_cyclic_namespace_alias_is_not_a_prefix(): + assert find_unexpected_compaction_keys({"ex:property": 1}, {"ex": "other", "other": "ex"}) == {"ex:property": 1} diff --git a/tests/unit/test_package_mode.py b/tests/unit/test_package_mode.py new file mode 100644 index 000000000..e9f7dc26f --- /dev/null +++ b/tests/unit/test_package_mode.py @@ -0,0 +1,346 @@ +"""Packaging mode is selected from the source, independently of graph contents.""" + +import io +import json +import zipfile +from pathlib import Path + +import pytest + +from rocrate_validator.events import Event, EventType +from rocrate_validator.models import ValidationSettings +from rocrate_validator.rocrate import ROCrate +from rocrate_validator.rocrate.package_type import PackageType +from rocrate_validator.services import __initialise_validator__, validate +from rocrate_validator.utils.http import HttpRequester +from rocrate_validator.utils.io_helpers.output.console import Console +from rocrate_validator.utils.io_helpers.output.json.formatters import format_validation_results +from rocrate_validator.utils.io_helpers.output.text.formatters import ValidationResultTextOutputFormatter +from rocrate_validator.utils.io_helpers.output.text.layout.report import ValidationReportLayout +from rocrate_validator.utils.uri import URI + + +@pytest.mark.parametrize("root_id", ["./", "https://example.org/crate", "urn:example:crate"]) +def test_directory_mode_does_not_depend_on_root_id_or_payload(tmp_path: Path, root_id: str): + descriptor = tmp_path / "ro-crate-metadata.json" + descriptor.write_text(json.dumps({"@graph": [{"@id": root_id, "@type": "Dataset"}]})) + crate = ROCrate.new_instance(tmp_path) + assert crate.package_type is PackageType.ATTACHED + assert crate.is_attached() and not crate.is_detached() + descriptor.write_text("not JSON") + assert ROCrate.new_instance(tmp_path).package_type is PackageType.ATTACHED + + +def test_missing_payload_does_not_change_attached_mode(tmp_path: Path): + (tmp_path / "ro-crate-metadata.json").write_text("{}") + crate = ROCrate.new_instance(tmp_path) + assert crate.package_type is PackageType.ATTACHED + (tmp_path / "payload.txt").write_text("data") + assert ROCrate.new_instance(tmp_path).package_type is PackageType.ATTACHED + + +@pytest.mark.parametrize("root_id", ["./", "https://example.org/crate"]) +def test_standalone_document_is_detached_even_with_dot_root(tmp_path: Path, root_id: str): + descriptor = tmp_path / "crate-ro-crate-metadata.json" + descriptor.write_text(json.dumps({"@graph": [{"@id": root_id, "@type": "Dataset"}]})) + crate = ROCrate.new_instance(descriptor) + assert crate.package_type is PackageType.DETACHED + assert crate.is_detached() and not crate.is_attached() + + +def test_zip_and_explicit_mode_constraints(tmp_path: Path): + archive = tmp_path / "crate.zip" + with zipfile.ZipFile(archive, "w") as output: + output.writestr("ro-crate-metadata.json", "{}") + assert ROCrate.new_instance(archive).package_type is PackageType.ATTACHED + with pytest.raises(ValueError, match="standalone metadata document"): + ROCrate.new_instance(archive, packaging_mode="detached") + with pytest.raises(ValueError, match="standalone metadata document"): + ROCrate.new_instance(tmp_path, packaging_mode="detached") + assert ROCrate.new_instance(archive, relative_root_path=Path("nested")).attached_descriptor_id == ( + "nested/ro-crate-metadata.json" + ) + + +def test_bagit_package_type_and_effective_descriptor(tmp_path: Path): + (tmp_path / "bagit.txt").write_text("BagIt-Version: 1.0\nTag-File-Character-Encoding: UTF-8\n") + (tmp_path / "data").mkdir() + (tmp_path / "data" / "ro-crate-metadata.json").write_text("{}") + crate = ROCrate.new_instance(tmp_path) + assert crate.package_type is PackageType.ATTACHED + assert crate.attached_descriptor_id == "data/ro-crate-metadata.json" + + +def test_extracted_zip_keeps_attached_mode_and_source_alive(tmp_path: Path): + archive = tmp_path / "crate.zip" + with zipfile.ZipFile(archive, "w") as output: + output.writestr("ro-crate-metadata.json", json.dumps(_minimal_metadata())) + settings = ValidationSettings( + rocrate_uri=archive, + profile_identifier="ro-crate-1.2", + requirement_severity="REQUIRED", + offline=True, + disable_remote_crate_download=False, + ) + validator = __initialise_validator__(settings) + result = validator.validate() + assert result.context.ro_crate.package_type is PackageType.ATTACHED + assert result.context.ro_crate.has_descriptor() + assert result.context.ro_crate.metadata.as_dict() == _minimal_metadata() + assert result.validation_settings.to_dict()["rocrate_uri"] == str(archive) + assert settings.rocrate_uri.as_path() == archive + + +def test_downloaded_zip_preserves_attached_mode_and_remote_source(monkeypatch, tmp_path: Path): + archive = tmp_path / "crate.zip" + with zipfile.ZipFile(archive, "w") as output: + output.writestr("ro-crate-metadata.json", json.dumps(_minimal_metadata())) + + class Response: + status_code = 200 + + def __init__(self): + self.raw = io.BytesIO(archive.read_bytes()) + + def __enter__(self): + return self + + def __exit__(self, *_args): + self.raw.close() + + remote_uri = "https://example.org/crate.zip" + settings = ValidationSettings( + rocrate_uri=remote_uri, + profile_identifier="ro-crate-1.2", + requirement_severity="REQUIRED", + packaging_mode="attached", + disable_remote_crate_download=False, + offline=True, + ) + monkeypatch.setattr(URI, "is_available", lambda self: True) + monkeypatch.setattr(HttpRequester().session, "get", lambda *_args, **_kwargs: Response()) + result = __initialise_validator__(settings).validate() + assert result.context.ro_crate.package_type is PackageType.ATTACHED + assert result.context.ro_crate.packaging_mode_explicit + assert result.validation_settings.to_dict()["rocrate_uri"] == remote_uri + assert result.context.ro_crate.has_descriptor() + + +def test_explicit_attached_canonical_document_uses_parent(tmp_path: Path): + descriptor = tmp_path / "ro-crate-metadata.json" + descriptor.write_text("{}") + crate = ROCrate.new_instance(descriptor, packaging_mode="attached") + assert crate.package_type is PackageType.ATTACHED + assert crate.packaging_mode_explicit + assert crate.uri.as_path() == tmp_path + + +def test_in_memory_source_has_no_implicit_directory(monkeypatch, tmp_path: Path): + metadata = {"@graph": [{"@id": "./", "@type": "Dataset"}]} + first = ROCrate.from_metadata_dict(metadata) + monkeypatch.chdir(tmp_path) + second = ROCrate.from_metadata_dict(metadata) + assert first.package_type is second.package_type is PackageType.UNSPECIFIED + assert not first.is_attached() and not first.is_detached() + assert str(first.uri) == str(second.uri) + assert not first.has_file(Path("payload.txt")) + assert ROCrate.from_metadata_dict(metadata, "detached").is_detached() + assert ROCrate.from_metadata_dict(metadata, "attached").is_attached() + assert ROCrate.from_metadata_dict({}).metadata.as_json() == "{}" + + +def test_settings_and_service_reject_invalid_context(tmp_path: Path): + with pytest.raises(ValueError, match="packaging_mode"): + ValidationSettings(rocrate_uri=tmp_path, packaging_mode="unknown", offline=True) + settings = ValidationSettings( + rocrate_uri=tmp_path, metadata_dict={"@graph": []}, packaging_mode="attached", offline=True + ) + with pytest.raises(ValueError, match="metadata_only"): + __initialise_validator__(settings) + with pytest.raises(ValueError, match="remote standalone"): + ROCrate.new_instance("https://example.org/ro-crate-metadata.json", packaging_mode="attached") + in_memory = __initialise_validator__({"metadata_dict": {}, "offline": True}) + assert in_memory.validation_settings.metadata_only is True + + +def _minimal_metadata(*, root_id: str = "./", file_id: str | None = None) -> dict: + graph = [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": root_id}, + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.2"}, + }, + { + "@id": root_id, + "@type": "Dataset", + "name": "Test crate", + "description": "A minimal test crate", + "datePublished": "2026-01-01", + "license": {"@id": "https://spdx.org/licenses/CC0-1.0"}, + }, + ] + if file_id is not None: + graph.append({"@id": file_id, "@type": "File", "name": "Test file"}) + return {"@context": {"@vocab": "http://schema.org/"}, "@graph": graph} + + +def _validate_metadata(metadata: dict, mode: str): + return validate( + { + "metadata_dict": metadata, + "metadata_only": True, + "packaging_mode": mode, + "offline": True, + "requirement_severity": "REQUIRED", + "profile_identifier": "ro-crate-1.2", + } + ) + + +def test_metadata_mode_controls_detached_rule_and_reports_scope(monkeypatch, tmp_path: Path): + metadata = _minimal_metadata(file_id="missing.txt") + first = _validate_metadata(metadata, "auto") + (tmp_path / "missing.txt").write_text("irrelevant") + monkeypatch.chdir(tmp_path) + second = _validate_metadata(metadata, "auto") + assert not any("not web-based" in issue.message for result in (first, second) for issue in result.issues) + assert first.skipped_checks_count == second.skipped_checks_count + assert any("package context is unavailable" in detail.message for detail in first.skipped_check_details) + assert first.context.publicID == second.context.publicID + assert first.to_dict()["validation_settings"]["package_type"] == "unspecified" + assert first.to_dict()["validation_settings"]["packaging_mode"] == "auto" + assert first.to_dict()["validation_settings"]["metadata_only"] is True + + attached = _validate_metadata(metadata, "attached") + detached = _validate_metadata(metadata, "detached") + assert not any("not web-based" in issue.message for issue in attached.issues) + assert not detached.passed() + assert any("not web-based" in issue.message for issue in detached.issues) + assert detached.to_dict()["validation_settings"]["packaging_mode_explicit"] is True + + +def test_detached_contextual_file_is_not_treated_as_payload(): + metadata = _minimal_metadata(file_id="#contextual-file") + result = _validate_metadata(metadata, "detached") + assert not any("not web-based" in issue.message for issue in result.issues) + + +@pytest.mark.parametrize("mode,expected", [("auto", "Unspecified"), ("attached", "Attached"), ("detached", "Detached")]) +def test_reports_show_package_type_after_target_profile(mode: str, expected: str): + result = _validate_metadata(_minimal_metadata(), mode) + console = Console(record=True, width=120) + layout = ValidationReportLayout(console, result.validation_settings) + _ = layout.layout + assert layout.base_info_layout is not None + + layout.subscribers[0].update(Event(EventType.VALIDATION_START), result.context) + assert f"[bold yellow]{expected}[/bold yellow]" in layout.base_info_layout.renderable.renderable + assert "[bold orange1]Metadata Only[/bold orange1]" in layout.base_info_layout.renderable.renderable + console.print(layout.base_info_layout.renderable) + interactive_text = console.export_text() + assert "Validation Severity:" in interactive_text + assert f"RO-Crate Package Type: {expected}" in interactive_text + assert interactive_text.index("RO-Crate:") < interactive_text.index("RO-Crate Package Type:") + assert interactive_text.index("RO-Crate Package Type:") < interactive_text.index("Target Profile:") + assert "Validation Scope: Metadata Only" in interactive_text + + report = json.loads(format_validation_results({"ro-crate-1.2": result})) + assert report["validation_settings"]["package_type"] == expected.lower() + assert report["validation_settings"]["packaging_mode"] == mode + assert report["validation_settings"]["metadata_only"] is True + assert result.to_dict()["validation_settings"]["package_type"] == expected.lower() + assert result.to_dict()["validation_settings"]["packaging_mode"] == mode + assert result.to_dict()["validation_settings"]["metadata_only"] is True + + text_console = Console(record=True, width=120) + text_console.print(ValidationResultTextOutputFormatter(result)) + text_report = text_console.export_text() + assert "Packaging mode:" not in text_report + assert "validation scope:" not in text_report + + +@pytest.mark.parametrize("metadata_only", [True, False]) +def test_interactive_report_shows_package_root_for_dictionary_input(tmp_path: Path, metadata_only: bool): + settings = ValidationSettings( + rocrate_uri=None, + metadata_dict=_minimal_metadata(), + package_root=tmp_path, + packaging_mode="attached", + metadata_only=metadata_only, + offline=True, + ) + console = Console(record=True, width=300) + layout = ValidationReportLayout(console, settings) + _ = layout.layout + assert layout.base_info_layout is not None + console.print(layout.base_info_layout.renderable) + report = console.export_text() + assert f"RO-Crate: {tmp_path}" in report + assert "in-memory metadata" not in report + + +def test_interactive_report_shows_in_memory_source_for_dictionary_only(): + settings = ValidationSettings(rocrate_uri=None, metadata_dict=_minimal_metadata(), metadata_only=True, offline=True) + console = Console(record=True, width=120) + layout = ValidationReportLayout(console, settings) + _ = layout.layout + assert layout.base_info_layout is not None + console.print(layout.base_info_layout.renderable) + assert "RO-Crate: in-memory metadata" in console.export_text() + + +def test_interactive_report_shows_full_package_scope(tmp_path: Path): + settings = ValidationSettings(rocrate_uri=tmp_path, metadata_only=False, offline=True) + console = Console(record=True, width=300) + layout = ValidationReportLayout(console, settings) + _ = layout.layout + assert layout.base_info_layout is not None + console.print(layout.base_info_layout.renderable) + report = console.export_text() + assert "Validation Scope: Full Package" in report + assert f"RO-Crate: {tmp_path}" in report + + +def test_attached_directory_missing_payload_stays_attached(tmp_path: Path): + (tmp_path / "ro-crate-metadata.json").write_text(json.dumps(_minimal_metadata(file_id="missing.txt"))) + settings = ValidationSettings( + rocrate_uri=tmp_path, + profile_identifier="ro-crate-1.2", + requirement_severity="REQUIRED", + offline=True, + ) + result = validate(settings) + assert result.context.ro_crate.package_type is PackageType.ATTACHED + assert any("missing.txt" in issue.message for issue in result.issues) + (tmp_path / "missing.txt").write_text("now available") + available_result = validate(settings) + assert available_result.context.ro_crate.package_type is PackageType.ATTACHED + assert not any("missing.txt" in issue.message for issue in available_result.issues) + + +def test_prefixed_descriptor_in_directory_does_not_become_detached(tmp_path: Path): + (tmp_path / "prefix-ro-crate-metadata.json").write_text(json.dumps(_minimal_metadata())) + settings = ValidationSettings( + rocrate_uri=tmp_path, + profile_identifier="ro-crate-1.2", + requirement_severity="REQUIRED", + offline=True, + ) + result = validate(settings) + assert result.context.ro_crate.package_type is PackageType.ATTACHED + assert any('file descriptor "ro-crate-metadata.json" is not present' in issue.message for issue in result.issues) + + +def test_ambiguous_attached_descriptors_are_not_selected_arbitrarily(tmp_path: Path): + (tmp_path / "first-ro-crate-metadata.json").write_text("{}") + (tmp_path / "second-ro-crate-metadata.json").write_text("{}") + crate = ROCrate.new_instance(tmp_path) + assert crate.package_type is PackageType.ATTACHED + assert crate.metadata_descriptor_id == "ro-crate-metadata.json" + assert not crate.has_descriptor() + + (tmp_path / "ro-crate-metadata.json").write_text("{}") + crate = ROCrate.new_instance(tmp_path) + assert crate.metadata_descriptor_id == "ro-crate-metadata.json" + assert crate.has_descriptor() diff --git a/tests/unit/test_rdf.py b/tests/unit/test_rdf.py new file mode 100644 index 000000000..6a707eb6e --- /dev/null +++ b/tests/unit/test_rdf.py @@ -0,0 +1,95 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from urllib.parse import urljoin + +from rdflib import Graph, Namespace, URIRef +from rdflib.namespace import RDF + +from rocrate_validator.utils.rdf import PREPARED_PROFILE_BASE, rebase_graph, rebase_node + + +def test_rebase_graph_maps_parent_relative_references_after_rdf_parsing(): + source_base = "https://example.org/a/b/crate/" + prepared_base = PREPARED_PROFILE_BASE + graph = Graph().parse( + data=""" + @prefix ex: <../> . + @prefix rdf: . + + ex:target rdf:type ex:Entity . + rdf:type ex:Entity . + """, + format="turtle", + publicID=source_base, + ) + + rebased = rebase_graph(graph, ((source_base, prepared_base),)) + + assert ( + URIRef("https://github.com/crs4/rocrate-validator/prepared/target"), + URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), + URIRef("https://github.com/crs4/rocrate-validator/prepared/Entity"), + ) in rebased + assert ( + URIRef("https://outside.example/entity"), + URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), + URIRef("https://github.com/crs4/rocrate-validator/prepared/Entity"), + ) in rebased + + +def test_rebase_graph_rebases_structural_node_when_used_as_target_class(): + prepared_base = PREPARED_PROFILE_BASE + crate_base = "file:///tmp/crate/" + sh = Namespace("http://www.w3.org/ns/shacl#") + class_shape = URIRef(f"{prepared_base}Dataset") + graph = Graph() + graph.add((class_shape, RDF.type, sh.NodeShape)) + graph.add((class_shape, sh.targetClass, class_shape)) + + rebased = rebase_graph( + graph, + ((prepared_base, crate_base),), + preserve_nodes=(class_shape,), + rebase_objects_for=(sh.targetClass,), + ) + + assert (class_shape, RDF.type, sh.NodeShape) in rebased + assert (class_shape, sh.targetClass, URIRef("file:///tmp/crate/Dataset")) in rebased + + +def test_rebase_node_preserves_parent_relative_trailing_slash(): + target_base = "file:///data/team/project/crate/" + source_uri = URIRef(urljoin(PREPARED_PROFILE_BASE, "../Dataset/")) + + rebased = rebase_node(source_uri, ((PREPARED_PROFILE_BASE, target_base),)) + + assert rebased == URIRef("file:///data/team/project/Dataset/") + + +def test_rebase_node_preserves_root_relative_path(): + target_base = "file:///data/team/project/crate/" + source_uri = URIRef("https://github.com/Dataset") + + rebased = rebase_node(source_uri, ((PREPARED_PROFILE_BASE, target_base),)) + + assert rebased == URIRef("file:///Dataset") + + +def test_rebase_node_leaves_absolute_iri_on_another_authority_unchanged(): + absolute_iri = URIRef("https://schema.org/Dataset") + + rebased = rebase_node(absolute_iri, ((PREPARED_PROFILE_BASE, "file:///data/crate/"),)) + + assert rebased == absolute_iri diff --git a/tests/unit/test_remote_context_retrieval.py b/tests/unit/test_remote_context_retrieval.py index 67f9fdf97..0deb4a119 100644 --- a/tests/unit/test_remote_context_retrieval.py +++ b/tests/unit/test_remote_context_retrieval.py @@ -18,6 +18,8 @@ import pytest +from rocrate_validator.utils.http import OfflineCacheMissError + @pytest.fixture(scope="module") def fd_format(): @@ -297,6 +299,16 @@ def test_check_remote_context_invalid(self, fd_format): finally: fd_format.HttpRequester = original_requester + def test_check_remote_context_propagates_offline_cache_miss(self, fd_format): + """Offline cache misses must reach the validation runner unchanged.""" + checker = object.__new__(fd_format.FileDescriptorJsonLdFormat) + checker.__get_remote_context__ = MagicMock( + side_effect=OfflineCacheMissError("https://example.com/context.json") + ) + + with pytest.raises(OfflineCacheMissError): + checker.__check_remote_context__("https://example.com/context.json") + class TestGetContextKeys: def test_get_context_keys_from_string(self, fd_format): diff --git a/tests/unit/test_remote_zip_ranges.py b/tests/unit/test_remote_zip_ranges.py new file mode 100644 index 000000000..fc466f6ab --- /dev/null +++ b/tests/unit/test_remote_zip_ranges.py @@ -0,0 +1,160 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import re +import zipfile +from pathlib import Path + +import pytest +from requests import HTTPError, Response + +from rocrate_validator.rocrate import BagitROCrate, ROCrate, ROCrateBagitRemoteZip, ROCrateRemoteZip +from rocrate_validator.utils.http import HttpRequester + +URL = "https://example.org/bagit.zip" +METADATA = b'{"@context": "https://w3id.org/ro/crate/1.1/context", "@graph": []}' + + +@pytest.fixture(name="remote_zip") +def remote_zip_fixture(monkeypatch): + """Model servers rejecting suffix ranges, without making any network requests.""" + + def install( + *, + comment=b"", + ignore_range=False, + status=206, + truncate=False, + bad_content_range=False, + compression=zipfile.ZIP_STORED, + bagit=True, + ): + buffer = io.BytesIO() + with zipfile.ZipFile(buffer, "w", compression=compression) as archive: + if bagit: + archive.writestr("bagit.txt", "BagIt-Version: 1.0\nTag-File-Character-Encoding: UTF-8\n") + archive.writestr("data/ro-crate-metadata.json", METADATA) + archive.writestr("data/large.bin", b"x" * 200_000) + archive.comment = comment + payload = buffer.getvalue() + ranges = [] + + def response(code, content=b"", **headers): + result = Response() + result.status_code = code + result.url = URL + result._content = content # pylint: disable=protected-access + result.headers.update(headers) + return result + + def head(_self, url, **_kwargs): + assert url == URL + if not _kwargs.get("allow_redirects"): + return response(302, **{"Content-Length": "0", "Location": URL + "?redirected"}) + return response(200, **{"Content-Length": str(len(payload))}) + + def get(_self, url, *, headers, **_kwargs): + assert url == URL + match = re.fullmatch(r"bytes=(\d+)-(\d+)", headers.get("Range", "")) + if not match: + return response(501) + start, end = map(int, match.groups()) + assert 0 <= start <= end < len(payload) + ranges.append((start, end)) + if ignore_range: + return response(200, payload[:-1] if truncate else payload) + content = payload[start : end + 1] + return response( + status, + content[:-1] if truncate else content, + **{"Content-Range": "bytes 0-0/1" if bad_content_range else f"bytes {start}-{end}/{len(payload)}"}, + ) + + monkeypatch.setattr(HttpRequester, "head", head, raising=False) + monkeypatch.setattr(HttpRequester, "get", get, raising=False) + return payload, ranges + + return install + + +@pytest.mark.parametrize("ignore_range", [False, True]) +@pytest.mark.parametrize("comment", [b"", b"Archive comment" * 100]) +def test_remote_zip_reads_entries_with_absolute_ranges(remote_zip, ignore_range, comment, caplog): + payload, ranges = remote_zip(comment=comment, ignore_range=ignore_range) + with caplog.at_level("WARNING", logger="rocrate_validator.utils.http"): + crate = ROCrateRemoteZip(URL) + + assert crate.has_file(Path("bagit.txt")) + assert crate.get_file_content(Path("data/ro-crate-metadata.json")) == METADATA + assert crate.get_file_content(Path("data/ro-crate-metadata.json"), binary_mode=False) == METADATA.decode() + assert crate.get_file_size(Path("data/large.bin")) == 200_000 + assert ranges + # Inspecting the directory and reading metadata must not download the large payload. + assert sum(end - start + 1 for start, end in ranges) < len(payload) + if ignore_range: + assert len(ranges) == 1, "Reuse the full response when the server ignores Range" + assert len(caplog.records) == 1 + assert "Server ignored HTTP Range" in caplog.text + assert URL in caplog.text + assert f"{len(payload)} bytes" in caplog.text + assert "retained in memory" in caplog.text + else: + assert not caplog.records + + +@pytest.mark.parametrize("ignore_range", [False, True]) +@pytest.mark.parametrize("bagit", [False, True]) +def test_remote_zip_bagit_detection_and_factory(remote_zip, ignore_range, bagit): + remote_zip(ignore_range=ignore_range, bagit=bagit) + + assert BagitROCrate.is_bagit_wrapping_crate(URL) is bagit + crate = ROCrate.new_instance(URL) + assert isinstance(crate, ROCrateRemoteZip) + assert isinstance(crate, ROCrateBagitRemoteZip) is bagit + path = "ro-crate-metadata.json" if bagit else "data/ro-crate-metadata.json" + assert crate.get_file_content(Path(path)) == METADATA + + +@pytest.mark.parametrize("status", [403, 404, 503]) +def test_remote_zip_propagates_range_http_errors(remote_zip, status): + remote_zip(status=status) + + with pytest.raises(HTTPError, match=str(status)): + ROCrateRemoteZip(URL) + + +@pytest.mark.parametrize("ignore_range", [False, True]) +def test_remote_zip_rejects_truncated_range_response(remote_zip, ignore_range): + remote_zip(truncate=True, ignore_range=ignore_range) + + with pytest.raises(OSError, match="range response"): + ROCrateRemoteZip(URL) + + +def test_remote_zip_rejects_wrong_content_range(remote_zip): + remote_zip(bad_content_range=True) + + with pytest.raises(OSError, match="range response"): + ROCrateRemoteZip(URL) + + +def test_remote_zip_reads_compressed_entries(remote_zip): + remote_zip(compression=zipfile.ZIP_DEFLATED) + crate = ROCrateRemoteZip(URL) + + assert crate.get_file_content(Path("data/ro-crate-metadata.json")) == METADATA + assert crate.get_file_content(Path("data/large.bin")) == b"x" * 200_000 + with pytest.raises(FileNotFoundError): + crate.get_file_content(Path("missing.txt")) diff --git a/tests/unit/test_requirement_lifecycle.py b/tests/unit/test_requirement_lifecycle.py index aff09a66b..3d21a5b7a 100644 --- a/tests/unit/test_requirement_lifecycle.py +++ b/tests/unit/test_requirement_lifecycle.py @@ -15,7 +15,15 @@ import pytest from rocrate_validator import services -from rocrate_validator.models import URI, RequirementLoader, Severity, ValidationContext, ValidationSettings +from rocrate_validator.errors import ValidationExecutionError +from rocrate_validator.models import ( + URI, + Requirement, + RequirementLoader, + Severity, + ValidationContext, + ValidationSettings, +) from tests.ro_crates import InvalidRootDataEntity @@ -138,3 +146,46 @@ def test_lifecycle_hooks_invoked_exactly_once_per_validation_run(lifecycle_spies f"{spy.__name__} should receive exactly one initialize+finalize " f"pair per validation run (got {events} across {runs} runs)" ) + + +def test_unexpected_check_error_is_wrapped_and_propagated(monkeypatch): + """Unexpected check failures must abort validation with their original cause.""" + + class ConcreteRequirement(Requirement): + @property + def hidden(self) -> bool: + return False + + def __init_checks__(self): + return [] + + requirement = object.__new__(ConcreteRequirement) + requirement._name = "Test requirement" + requirement._checks = [] + requirement._path = None + check = type("Check", (), {})() + check.identifier = "test-check" + check.depends_on = [] + check.requirement = requirement + requirement._checks = [check] + + context = type("Context", (), {})() + context.settings = type("Settings", (), {"skip_checks": []})() + context.aborted = False + context.maybe_warn_offline_cache_miss = lambda exc: False + + def is_check_skipped(_candidate: object) -> bool: + """Report that the isolated execution test has no configured skips.""" + return False + + context.is_check_skipped = is_check_skipped + + def fail_check(*args): + raise RuntimeError("unexpected check failure") + + monkeypatch.setattr(Requirement, "__execute_check__", fail_check) + + with pytest.raises(ValidationExecutionError, match="test-check") as exc_info: + requirement._do_validate_(context) + + assert isinstance(exc_info.value.__cause__, RuntimeError) diff --git a/tests/unit/test_rocrate.py b/tests/unit/test_rocrate.py index 39bf4099f..9fbb81540 100644 --- a/tests/unit/test_rocrate.py +++ b/tests/unit/test_rocrate.py @@ -19,6 +19,8 @@ import pytest from rocrate_validator.errors import ROCrateInvalidURIError +from rocrate_validator.models import URI, ValidationSettings, Validator +from rocrate_validator.models import profile_detection as profile_detection_module from rocrate_validator.rocrate import ( BagitROCrate, ROCrate, @@ -31,6 +33,7 @@ ROCrateMetadata, ROCrateRemoteZip, ) +from rocrate_validator.rocrate import metadata as metadata_module from rocrate_validator.utils import log as logging from tests.ro_crates import InvalidDataEntity, ValidROC @@ -224,12 +227,12 @@ def test_valid_local_rocrate(): # test file size size = roc.get_file_size(metadata_file_descriptor) - assert size == 26788, "Size should be 26788" + assert size == 26708, "Size should be 26708" # test crate size — updated after `f8d16ba6` trimmed trailing whitespace # from `index.html` / `ro-crate-preview.html` (~6.7 KB delta from the # historical 311817). - assert roc.size == 305049, "Size should be 305049" + assert roc.size == 304969, "Size should be 304969" # test get_file_content binary mode content = roc.get_file_content(metadata_file_descriptor) @@ -586,6 +589,127 @@ def _metadata_dict_with_id(entity_id: str) -> dict: } +def _metadata_dict_with_main_entity(main_entity: object) -> dict: + """Build a minimal RO-Crate metadata dict with a configurable mainEntity.""" + return { + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + }, + { + "@id": "./", + "@type": "Dataset", + "mainEntity": main_entity, + }, + { + "@id": "workflow.ga", + "@type": "File", + }, + ], + } + + +@pytest.mark.parametrize( + ("entity_id", "getter_name", "location"), + [ + ("ro-crate-metadata.json", "get_conforms_to", "RO-Crate Metadata Descriptor"), + ("./", "get_root_data_entity_conforms_to", "Root Data Entity"), + ], +) +def test_conforms_to_profile_detection_warns_and_skips_string_values(entity_id, getter_name, location, monkeypatch): + """String literals are diagnosed and excluded while valid @id references remain detectable.""" + warnings = [] + monkeypatch.setattr(metadata_module.logger, "warning", lambda message, *args: warnings.append(message % args)) + metadata_dict = _metadata_dict_with_id("remote-file") + metadata_dict["@graph"][0]["conformsTo"] = {"@id": "https://w3id.org/ro/crate/1.1"} + for entity in metadata_dict["@graph"]: + if entity["@id"] == entity_id: + entity["conformsTo"] = [ + {"@id": "https://example.org/profile"}, + "https://example.org/literal-profile", + ] + break + + crate = ROCrate.from_metadata_dict(metadata_dict) + + assert getattr(crate.metadata, getter_name)() == ["https://example.org/profile"] + assert len(warnings) == 1 + assert "Cannot use" in warnings[0] + assert location in warnings[0] + assert "https://example.org/literal-profile" in warnings[0] + assert "containing `@id`" in warnings[0] + + +def test_automatic_profile_detection_reports_string_conforms_to(monkeypatch): + """Automatic detection warns about a literal URI without selecting it.""" + warnings = [] + monkeypatch.setattr( + profile_detection_module.logger, + "warning", + lambda message, *args: warnings.append(message % args), + ) + metadata_dict = { + "@context": "https://w3id.org/ro/crate/1.2/context", + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": {"@id": "./"}, + "conformsTo": "https://w3id.org/ro/crate/1.2", + }, + {"@id": "./", "@type": "Dataset", "name": "Test crate"}, + ], + } + settings = ValidationSettings( + rocrate_uri=URI(ValidROC().wrroc_paper), + metadata_dict=metadata_dict, + metadata_only=True, + ) + + assert Validator(settings).detect_rocrate_profiles() == [] + assert len(warnings) == 1 + assert "RO-Crate Metadata Descriptor" in warnings[0] + assert "https://w3id.org/ro/crate/1.2" in warnings[0] + + +@pytest.mark.parametrize("main_entity", [{"@id": "workflow.ga"}, [{"@id": "workflow.ga"}]]) +def test_get_main_workflow_returns_entity_for_object_and_singleton_array(main_entity): + """A singular main workflow accessor accepts both JSON-LD representations.""" + crate = ROCrate.from_metadata_dict(_metadata_dict_with_main_entity(main_entity)) + root_data_entity = crate.metadata.get_root_data_entity() + + raw_main_entity = root_data_entity.get_property("mainEntity") + main_workflow = crate.metadata.get_main_workflow() + + if isinstance(main_entity, list): + assert isinstance(raw_main_entity, list), "Generic property access must preserve list values" + else: + assert isinstance(raw_main_entity, ROCrateEntity), "Object values must resolve to an entity" + assert isinstance(main_workflow, ROCrateEntity), "The singular accessor must return one entity" + assert main_workflow.id == "workflow.ga" + + +@pytest.mark.parametrize( + "main_entity", + [ + [], + [{"@id": "workflow.ga"}, {"@id": "other-workflow.ga"}], + "workflow.ga", + {"name": "workflow.ga"}, + {"@id": 42}, + ], +) +def test_get_main_workflow_rejects_invalid_values(main_entity): + """Invalid mainEntity cardinality and value shapes raise controlled errors.""" + crate = ROCrate.from_metadata_dict(_metadata_dict_with_main_entity(main_entity)) + + with pytest.raises((TypeError, ValueError), match="mainEntity"): + crate.metadata.get_main_workflow() + + @pytest.mark.parametrize( "entity_id", [ @@ -623,6 +747,39 @@ def test_absolute_uri_data_entity_is_classified_as_remote(entity_id): ) +@pytest.mark.parametrize("entity_id", ["example1/", "index.html"]) +def test_explicit_jsonld_base_resolves_relative_data_entity_id(entity_id): + """Relative IDs with an HTTPS @base identify web data entities (issue #240).""" + metadata_dict = _metadata_dict_with_id(entity_id) + metadata_dict["@context"] = [ + "https://w3id.org/ro/crate/1.2/context", + {"@base": "https://www.researchobject.org/workflow-run-crate/profiles/0.5/process_run_crate/"}, + ] + crate = ROCrate.from_metadata_dict(metadata_dict) + entity = crate.metadata.get_entity(entity_id) + + assert entity is not None + assert entity.id_as_uri.uri == ( + "https://www.researchobject.org/workflow-run-crate/profiles/0.5/process_run_crate/" + entity_id + ) + assert entity.is_remote() + assert entity in crate.metadata.get_web_data_entities() + assert entity not in crate.metadata.get_data_entities(exclude_web_data_entities=True) + + +def test_last_explicit_jsonld_base_takes_precedence(): + metadata_dict = _metadata_dict_with_id("index.html") + metadata_dict["@context"] = [ + {"@base": "https://example.org/old/"}, + {"@base": "https://example.org/current/"}, + ] + crate = ROCrate.from_metadata_dict(metadata_dict) + entity = crate.metadata.get_entity("index.html") + + assert entity is not None + assert entity.id_as_uri.uri == "https://example.org/current/index.html" + + @pytest.mark.parametrize( "entity_id", [ diff --git a/tests/unit/test_services.py b/tests/unit/test_services.py index 009d8223a..fda9e29d7 100644 --- a/tests/unit/test_services.py +++ b/tests/unit/test_services.py @@ -16,12 +16,22 @@ import shutil import tempfile from pathlib import Path +from types import SimpleNamespace + +import pytest from rocrate_validator.models import URI, ValidationSettings from rocrate_validator.rocrate import ROCrateMetadata -from rocrate_validator.services import detect_profiles, get_profiles, validate +from rocrate_validator.services import ( + detect_profiles, + detect_profiles_detailed, + get_profiles, + validate, + validate_metadata_as_dict, +) from rocrate_validator.utils import log as logging from tests.ro_crates import InvalidFileDescriptorEntity, InvalidMultiProfileROC, ValidROC +from tests.shared import RO_CRATE_1_1_PROFILE_IDENTIFIER # set up logging logger = logging.getLogger(__name__) @@ -30,6 +40,93 @@ metadata_file_descriptor = Path(ROCrateMetadata.METADATA_FILE_DESCRIPTOR) +def _profile_detection_settings(about, conforms_to=None): + descriptor = {"@id": "ro-crate-metadata.json", "@type": "CreativeWork", "about": about} + if conforms_to is not None: + descriptor["conformsTo"] = conforms_to + return ValidationSettings( + rocrate_uri=URI("/tmp/conforms-profile-detection"), + metadata_only=True, + metadata_dict={ + "@context": {}, + "@graph": [descriptor, {"@id": "./", "@type": "Dataset"}], + }, + ) + + +def test_detailed_profile_detection_reports_missing_and_unknown_hints(monkeypatch): + """Detailed detection distinguishes absent hints from unmatched profile references.""" + monkeypatch.setattr("rocrate_validator.models.profile.Profile.load_profiles", lambda *args, **kwargs: []) + + missing = detect_profiles_detailed(_profile_detection_settings({"@id": "./"})) + unknown = detect_profiles_detailed( + _profile_detection_settings({"@id": "./"}, {"@id": "https://example.org/unknown-profile"}) + ) + + assert [diagnostic.code for diagnostic in missing.diagnostics] == ["NO_PROFILE_HINT"] + assert [diagnostic.code for diagnostic in unknown.diagnostics] == ["PROFILE_NOT_FOUND"] + assert unknown.diagnostics[0].value == "https://example.org/unknown-profile" + + +def test_detailed_profile_detection_handles_invalid_about_without_raising(monkeypatch): + """A malformed about value is returned as a structured diagnostic.""" + monkeypatch.setattr("rocrate_validator.models.profile.Profile.load_profiles", lambda *args, **kwargs: []) + + result = detect_profiles_detailed(_profile_detection_settings("./")) + + assert {diagnostic.code for diagnostic in result.diagnostics} == {"INVALID_REFERENCE", "NO_PROFILE_HINT"} + + +@pytest.mark.parametrize("location", ["descriptor", "root"]) +@pytest.mark.parametrize("as_list", [False, True]) +def test_detailed_profile_detection_suggests_matching_string_hint(monkeypatch, location, as_list): + """String URIs suggest profiles without selecting them or repairing metadata.""" + profile = SimpleNamespace( + uri="https://w3id.org/ro/crate/1.2", + identifier="ro-crate-1.2", + inherited_profiles=[], + ) + monkeypatch.setattr("rocrate_validator.models.profile.Profile.load_profiles", lambda *args, **kwargs: [profile]) + + value = [profile.uri] if as_list else profile.uri + settings = _profile_detection_settings({"@id": "./"}) + assert settings.metadata_dict is not None + entity = settings.metadata_dict["@graph"][0 if location == "descriptor" else 1] + entity["conformsTo"] = value + result = detect_profiles_detailed(settings) + + assert result.profiles == [] + assert detect_profiles(settings) == [] + assert result.suggested_profile is profile + assert result.diagnostics[0].code == "INVALID_CONFORMS_TO" + assert "not a string" in result.diagnostics[0].message + assert f'{{"@id": "{profile.uri}"}}' in result.diagnostics[0].message + assert entity["conformsTo"] == value + + +def test_profile_detection_does_not_select_strings_alongside_valid_references(monkeypatch): + """Valid references take precedence over malformed string hints.""" + base = SimpleNamespace(uri="https://example.org/base", identifier="base", inherited_profiles=[]) + specific = SimpleNamespace(uri="https://example.org/specific", identifier="specific", inherited_profiles=[base]) + monkeypatch.setattr( + "rocrate_validator.models.profile.Profile.load_profiles", lambda *args, **kwargs: [base, specific] + ) + settings = _profile_detection_settings( + {"@id": "./"}, + [{"@id": base.uri}, specific.uri, "https://example.org/unknown"], + ) + + result = detect_profiles_detailed(settings) + + assert result.profiles == [base] + assert result.suggested_profile is None + assert [diagnostic.code for diagnostic in result.diagnostics] == [ + "INVALID_CONFORMS_TO", + "INVALID_CONFORMS_TO", + ] + assert result.diagnostics[-1].location == "RO-Crate Metadata Descriptor" + + def test_default_profiles_list(): """ Test the list of profiles. @@ -107,7 +204,14 @@ def test_disable_inherited_profiles_issue_reporting(): logger.debug("Validating a local RO-Crate: %s", crate_path) # First, validate with inherited profiles issue reporting enabled - settings = ValidationSettings(rocrate_uri=URI(crate_path), disable_inherited_profiles_issue_reporting=False) + # Pinned to the profile the crate declares: the assertions below are about + # issues being attributed to the main profile rather than to the inherited + # ones, so validating against anything else would make them vacuous. + settings = ValidationSettings( + rocrate_uri=URI(crate_path), + profile_identifier="workflow-testing-ro-crate-0.1", + disable_inherited_profiles_issue_reporting=False, + ) result = validate(settings) total_issues_with_inheritance = len(result.get_issues()) logger.debug("Total issues with inherited profiles issue reporting enabled: %d", total_issues_with_inheritance) @@ -135,7 +239,7 @@ def test_skip_pycheck_on_workflow_ro_crate(): # Set the rocrate_uri to the workflow testing RO-Crate crate_path = InvalidFileDescriptorEntity().invalid_conforms_to logger.debug("Validating a local RO-Crate: %s", crate_path) - settings = ValidationSettings(rocrate_uri=URI(crate_path)) + settings = ValidationSettings(rocrate_uri=URI(crate_path), profile_identifier=RO_CRATE_1_1_PROFILE_IDENTIFIER) result = validate(settings) assert not result.passed(), ( "The RO-Crate is expected to be invalid because of an incorrect conformsTo field and missing resources" @@ -186,7 +290,11 @@ def test_valid_crate_folder_with_metadata_only(): shutil.copy(metadata_src, metadata_dst) # Define shared settings object - settings = ValidationSettings(rocrate_uri=URI(Path(tmpdirname)), metadata_only=True) + settings = ValidationSettings( + rocrate_uri=URI(Path(tmpdirname)), + profile_identifier=RO_CRATE_1_1_PROFILE_IDENTIFIER, + metadata_only=True, + ) profiles = detect_profiles(settings) @@ -211,7 +319,8 @@ def test_valid_crate_metadata_dict_with_metadata_only(): # Define shared settings object settings = ValidationSettings( # type: ignore[call-arg] # rocrate_uri not needed in metadata-dict mode - metadata_dict=metadata_dict + metadata_dict=metadata_dict, + profile_identifier=RO_CRATE_1_1_PROFILE_IDENTIFIER, ) profiles = detect_profiles(settings) @@ -222,7 +331,5 @@ def test_valid_crate_metadata_dict_with_metadata_only(): # Check the detected profile assert profiles[0].identifier == "ro-crate-1.1", "Expected the 'ro-crate' profile" - from rocrate_validator.services import validate_metadata_as_dict - result = validate_metadata_as_dict(metadata_dict, settings) assert result.passed(), "RO-Crate should be valid in metadata-only mode" diff --git a/tests/unit/test_skipped_check_details.py b/tests/unit/test_skipped_check_details.py new file mode 100644 index 000000000..0852d52a3 --- /dev/null +++ b/tests/unit/test_skipped_check_details.py @@ -0,0 +1,92 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from types import SimpleNamespace + +from rocrate_validator.models import CheckResult, SkipCategory, ValidationResult + + +def _result(check): + result = object.__new__(ValidationResult) + result._check_results = {} + result._executed_checks = set() + result._executed_checks_results = {} + result._skipped_checks = set() + result._skipped_check_details = {} + result._statistics = SimpleNamespace(checks={check}) + return result + + +def _check(): + check = SimpleNamespace( + identifier="test_1.1", + name="dependent", + severity=SimpleNamespace(name="REQUIRED"), + requirement=SimpleNamespace( + name="Dependent requirement", + profile=SimpleNamespace(identifier="test"), + ), + ) + return type("TestCheck", (), check.__dict__)() + + +def test_skipped_check_detail_preserves_reason_and_category(): + check = _check() + result = _result(check) + + result._record_check_result(check, CheckResult.SKIPPED, "base failed", SkipCategory.DEPENDENCY) + + assert result.skipped_check_details[0].category is SkipCategory.DEPENDENCY + assert result.skipped_check_details[0].to_dict() == { + "identifier": "test_1.1", + "profile": "test", + "requirement": "Dependent requirement", + "name": "dependent", + "severity": "REQUIRED", + "message": "base failed", + "category": "dependency", + } + + +def test_skipped_check_detail_is_removed_when_check_passes(): + check = _check() + result = _result(check) + + result.record_skip(check, "base skipped", SkipCategory.DEPENDENCY) + result._record_check_result(check, CheckResult.PASSED) + + assert result.skipped_check_details == [] + + +def test_record_skip_normalizes_string_category_to_enum(): + check = _check() + result = _result(check) + + result.record_skip(check, "metadata descriptor is not available", "exception") + + assert result.skipped_check_details[0].category is SkipCategory.EXCEPTION + + +def test_skip_category_has_stable_values_and_descriptions(): + assert {category.value for category in SkipCategory} == { + "returned", + "configured", + "exception", + "deactivated", + "dependency", + "not_reached", + "inherited", + } + assert all(category.description for category in SkipCategory) + assert SkipCategory.DEPENDENCY.description == "The check was skipped because a dependency did not pass." diff --git a/tests/unit/test_uri.py b/tests/unit/test_uri.py index bfd26a414..ab017b824 100644 --- a/tests/unit/test_uri.py +++ b/tests/unit/test_uri.py @@ -229,5 +229,50 @@ def test_rocrate_uri_remote_invalid(): assert str(excinfo.value) == f'The RO-crate at the URI "{uri}" is not available' +@pytest.mark.parametrize("name", ["my crate", "my-crate", "caffè", "a+b", "100%", "my%20crate", "a#b?c"]) +@pytest.mark.parametrize("representation", ["path", "string", "uri", "localhost"]) +def test_local_path_and_file_uri_round_trip(tmp_path, name, representation): + path = tmp_path / name + path.mkdir() + values = { + "path": path, + "string": str(path), + "uri": path.as_uri(), + "localhost": path.as_uri().replace("file://", "file://localhost", 1), + } + uri = URI(values[representation]) + assert uri.as_path() == path + assert uri.is_local_directory() + assert uri.is_available() + assert uri.base_uri == path.as_uri() + assert str(uri) == str(values[representation]) + + +def test_literal_percent_path_does_not_select_space_directory(tmp_path): + (tmp_path / "my crate").mkdir() + literal = tmp_path / "my%20crate" + for value in (literal, str(literal), literal.as_uri()): + uri = URI(value) + assert uri.as_path() == literal + assert not uri.is_available() + + +def test_relative_local_base_and_symlink_preserve_path(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + target = tmp_path / "actual crate" + target.mkdir() + link = tmp_path / "linked crate" + link.symlink_to(target, target_is_directory=True) + uri = URI("linked crate") + assert uri.as_path() == type(tmp_path)("linked crate") + assert uri.base_uri == link.as_uri() + assert uri.is_available() + + +def test_remote_file_authority_cannot_be_converted_to_local_path(): + with pytest.raises(ValueError, match="not a local resource"): + URI("file://other-host/tmp/my%20crate").as_path() + + if __name__ == "__main__": unittest.main() diff --git a/tests/unit/test_validation_settings.py b/tests/unit/test_validation_settings.py index 7f4284e11..bb27378cf 100644 --- a/tests/unit/test_validation_settings.py +++ b/tests/unit/test_validation_settings.py @@ -70,6 +70,7 @@ def test_validation_settings_to_dict(): assert settings_dict["profiles_path"] == "/path/to/profiles" assert settings_dict["requirement_severity"] == Severity.RECOMMENDED assert settings_dict["enable_profile_inheritance"] is False + assert settings_dict["extra_checks"] is False def test_validation_settings_enable_profile_inheritance(): @@ -84,6 +85,11 @@ def test_validation_settings_disable_inherited_profiles_issue_reporting(): settings = ValidationSettings() # type: ignore[call-arg] assert settings.disable_inherited_profiles_issue_reporting is False + +def test_validation_settings_profile_checks(): + settings = ValidationSettings(validate_profile_checks=False) # type: ignore[call-arg] + assert settings.validate_profile_checks is False + settings = ValidationSettings(disable_inherited_profiles_issue_reporting=True) # type: ignore[call-arg] assert settings.disable_inherited_profiles_issue_reporting is True diff --git a/tests/unit/test_validation_statistics.py b/tests/unit/test_validation_statistics.py new file mode 100644 index 000000000..d18133013 --- /dev/null +++ b/tests/unit/test_validation_statistics.py @@ -0,0 +1,273 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +import pytest +from rich.console import Console + +from rocrate_validator import services +from rocrate_validator.events import EventType, Subscriber +from rocrate_validator.models import ( + CheckResult, + SkipCategory, + ValidationContext, + ValidationSettings, + ValidationStatistics, + Validator, +) +from rocrate_validator.requirements.shacl.checks import SHACLCheck +from rocrate_validator.requirements.shacl.validator import SHACLValidator +from rocrate_validator.utils.io_helpers.output.text.formatters import ValidationResultTextOutputFormatter +from rocrate_validator.utils.io_helpers.output.text.layout.progress import ProgressMonitor + + +@pytest.fixture +def statistics_settings(tmp_path: Path): + """A small overlay with inherited, deactivated, hidden and target-local checks.""" + profiles = tmp_path / "profiles" + for token in ("base", "overlay", "wrapper"): + profile = profiles / token + profile.mkdir(parents=True) + inheritance = "prof:isProfileOf ; validator:isRuleOverlayOf ;" if token != "base" else "" + (profile / "profile.ttl").write_text( + f""" + @prefix prof: . + @prefix rdfs: . + @prefix validator: . + a prof:Profile; prof:hasToken "{token}"; + rdfs:label "{token}"; {inheritance} . + """, + encoding="utf-8", + ) + prefixes = """ + @prefix sh: . + @prefix schema: . + @prefix validator: . + """ + (profiles / "base" / "shapes.ttl").write_text( + prefixes + + """ + a sh:NodeShape; sh:name "Name"; sh:targetNode ; + sh:property [sh:path schema:name; sh:name "Required name"; sh:minCount 1] . + a sh:NodeShape; sh:name "Disabled"; sh:targetNode ; + sh:deactivated true; sh:class schema:Dataset . + a sh:NodeShape, validator:HiddenShape; sh:name "Hidden"; + sh:targetNode ; sh:class schema:Thing . + """, + encoding="utf-8", + ) + (profiles / "overlay" / "shapes.ttl").write_text( + prefixes + + """ + a sh:NodeShape; sh:name "Description"; sh:targetNode ; + sh:property [sh:path schema:description; sh:name "Required description"; sh:minCount 1] . + """, + encoding="utf-8", + ) + return ValidationSettings( + rocrate_uri=tmp_path, + profiles_path=profiles, + profile_identifier="overlay", + metadata_only=True, + metadata_dict={ + "@context": {"@vocab": "http://schema.org/"}, + "@graph": [{"@id": "urn:item", "@type": "Thing", "name": "Example", "description": "Example"}], + }, + ) + + +class _StatisticsTrace(Subscriber): + def __init__(self): + super().__init__("statistics trace") + self.skipped_counts = [] + self.profile_snapshots = {} + + def update(self, event, ctx=None): + statistics = ctx.result.statistics + self.skipped_counts.append(statistics.total_skipped_checks) + if event.event_type is EventType.PROFILE_VALIDATION_END: + self.profile_snapshots[event.profile.identifier] = ( + set(statistics.validated_checks), + set(statistics.skipped_checks), + ) + + +class _ProgressTrace(ProgressMonitor): + def __init__(self, settings, stats=None): + super().__init__(settings, stats) + self.snapshots = [] + + def update(self, event, ctx=None): + super().update(event, ctx) + self.snapshots.append([(task.completed, task.total) for task in self.progress.tasks]) + + +def test_fully_overridden_requirements_are_not_counted(tmp_path: Path): + settings = ValidationSettings( + rocrate_uri=tmp_path, + profile_identifier="ro-crate-1.2", + ) + statistics = ValidationStatistics(settings) + overridden_requirements = [ + requirement + for profile in statistics.profiles + for requirement in profile.get_requirements(settings.requirement_severity) + if requirement.overridden + ] + + assert overridden_requirements + assert all(not requirement.overridden for requirement in statistics.requirements) + assert statistics.total_requirements == len(statistics.requirements) + + +@pytest.mark.parametrize("profile", ["overlay", "wrapper"]) +def test_deferred_checks_are_not_reported_as_skipped(statistics_settings, profile): + statistics_settings.profile_identifier = profile + trace = _StatisticsTrace() + result = services.validate(statistics_settings, subscribers=[trace]) + + assert result.passed() + assert trace.skipped_counts == sorted(trace.skipped_counts) + assert result.statistics.total_skipped_checks == result.skipped_checks_count == 1 + assert result.skipped_check_details[0].category is SkipCategory.DEACTIVATED + validated, skipped = trace.profile_snapshots["base"] + assert not validated # inherited SHACL checks are still pending + assert {check.name for check in skipped} == {"Disabled"} + assert set(result.statistics.validated_checks) | set(result.statistics.skipped_checks) == result.statistics.checks + + +@pytest.mark.parametrize("profile", ["overlay", "wrapper"]) +def test_live_and_final_progress_count_unique_processed_checks(statistics_settings, profile): + statistics_settings.profile_identifier = profile + monitor = _ProgressTrace(statistics_settings) + result = services.validate(statistics_settings, subscribers=[monitor]) + final_monitor = ProgressMonitor(statistics_settings, result.statistics) + + assert all(completed <= total for snapshot in monitor.snapshots for completed, total in snapshot) + assert [(task.completed, task.total) for task in monitor.progress.tasks] == [ + (task.completed, task.total) for task in final_monitor.progress.tasks + ] + assert all(task.completed == task.total for task in monitor.progress.tasks) + + +@pytest.mark.parametrize("effective_identifier", [False, True]) +def test_configured_skip_is_included_in_totals_and_preserved(statistics_settings, effective_identifier): + context = ValidationContext(Validator(statistics_settings), statistics_settings) + source = next(profile for profile in context.profiles if profile.identifier == "base") + check = source.get_requirement_check("Required name") + statistics_settings.skip_checks = [ + context.effective_check_identifier(check) if effective_identifier else check.identifier + ] + result = services.validate(statistics_settings) + statistics = result.statistics + + assert {detail.check.name for detail in result.skipped_check_details} == {"Required name", "Disabled"} + assert statistics.total_skipped_checks == result.skipped_checks_count == 2 + assert set(statistics.validated_checks) | set(statistics.skipped_checks) == statistics.checks + + +def test_batched_target_failures_are_counted(statistics_settings): + del statistics_settings.metadata_dict["@graph"][0]["description"] + result = services.validate(statistics_settings) + + assert not result.passed() + assert {check.name for check in result.statistics.failed_checks} == {"Required description"} + assert set(result.statistics.failed_checks) == result.failed_checks + assert all(result.get_check_result(check) is CheckResult.FAILED for check in result.statistics.failed_checks) + statistics = result.statistics + assert len(statistics.validated_checks) + statistics.total_skipped_checks == statistics.total_checks + + +def test_text_report_uses_effective_overlay_check_identity(statistics_settings): + """Text reports identify inherited checks in the active overlay profile.""" + del statistics_settings.metadata_dict["@graph"][0]["name"] + result = services.validate(statistics_settings) + issue = result.get_issues()[0] + effective_identifier = result.context.effective_check_identifier(issue.check) + + console = Console(record=True, color_system=None, width=120) + console.print(ValidationResultTextOutputFormatter(result)) + report = console.export_text() + + assert issue.source_profile_identifier == "base" + assert issue.profile_identifier == "overlay" + assert issue.identifier == effective_identifier + assert effective_identifier in report + assert issue.check.identifier not in report + + +@pytest.mark.parametrize("abort", [False, True]) +def test_deferred_checks_become_skipped_when_validation_stops(statistics_settings, monkeypatch, abort): + statistics_settings.abort_on_first = not abort + execute_check = SHACLCheck.execute_check + + def stop_at_target(check, context): + if check.requirement.profile.identifier == "overlay": + context.result.add_issue("Stop before the combined SHACL run", check) + if abort: + context.abort_validation("Input is unavailable") + return False + return execute_check(check, context) + + def unexpected_batch(*args, **kwargs): + pytest.fail("SHACL must not run after validation has stopped") + + monkeypatch.setattr(SHACLCheck, "execute_check", stop_at_target) + monkeypatch.setattr(SHACLValidator, "validate", unexpected_batch) + trace = _StatisticsTrace() + result = services.validate(statistics_settings, subscribers=[trace]) + statistics = result.statistics + + assert not result.passed() + assert trace.skipped_counts == sorted(trace.skipped_counts) + assert { + detail.check.name for detail in result.skipped_check_details if detail.category is SkipCategory.NOT_REACHED + } == {"Check Name", "Required name", "Required description"} + assert not statistics.passed_checks + assert statistics.total_skipped_checks == result.skipped_checks_count == 4 + assert set(statistics.validated_checks) | set(statistics.skipped_checks) == statistics.checks + assert result.to_dict()["skipped_checks"] == len(result.to_dict()["skipped_check_details"]) == 4 + + +@pytest.mark.parametrize("missing_property", ["name", "description"]) +def test_fail_fast_batch_keeps_failed_and_skipped_counts_disjoint(statistics_settings, missing_property): + statistics_settings.abort_on_first = True + del statistics_settings.metadata_dict["@graph"][0][missing_property] + result = services.validate(statistics_settings) + statistics = result.statistics + + assert not result.passed() + assert len(statistics.failed_checks) == 1 + assert set(statistics.failed_checks) == result.failed_checks + assert set(statistics.validated_checks).isdisjoint(statistics.skipped_checks) + assert statistics.total_skipped_checks == result.skipped_checks_count + assert set(statistics.validated_checks) | set(statistics.skipped_checks) == statistics.checks + + +def test_suppressed_inherited_checks_do_not_inflate_progress(statistics_settings): + profile_path = statistics_settings.profiles_path / "overlay" / "profile.ttl" + profile_path.write_text( + profile_path.read_text(encoding="utf-8").replace("validator:isRuleOverlayOf ;", ""), + encoding="utf-8", + ) + statistics_settings.disable_inherited_profiles_issue_reporting = True + monitor = _ProgressTrace(statistics_settings) + result = services.validate(statistics_settings, subscribers=[monitor]) + + assert result.passed() + assert result.statistics.total_checks == 2 + assert result.statistics.total_skipped_checks == result.skipped_checks_count == 0 + assert all(task.completed == task.total for task in monitor.progress.tasks) + assert all(completed <= total for snapshot in monitor.snapshots for completed, total in snapshot) diff --git a/tests/unit/test_versioning.py b/tests/unit/test_versioning.py new file mode 100644 index 000000000..017c2a12e --- /dev/null +++ b/tests/unit/test_versioning.py @@ -0,0 +1,125 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import shutil +import subprocess +import sys +from pathlib import Path +from unittest.mock import Mock + +import pytest + +from rocrate_validator.utils import versioning + +PACKAGE_VERSION = versioning.get_config()["tool"]["poetry"]["version"] + + +def _version_config(version: str = PACKAGE_VERSION) -> dict: + return {"tool": {"poetry": {"version": version}}} + + +def _run_git(cwd: Path, *args: str) -> None: + subprocess.run(["git", *args], cwd=cwd, check=True, capture_output=True, text=True) + + +def test_get_version_uses_distribution_metadata_outside_source_checkout(monkeypatch, tmp_path: Path): + package_root = tmp_path / "site-packages" + package_root.mkdir() + monkeypatch.setattr(versioning, "get_config_path", lambda: package_root / "pyproject.toml") + monkeypatch.setattr(versioning, "_is_source_checkout", lambda _: False) + monkeypatch.setattr(versioning.metadata, "version", lambda name: PACKAGE_VERSION) + run_git_command = Mock(side_effect=AssertionError("Git must not be queried for an installed package")) + get_config = Mock(side_effect=AssertionError("pyproject.toml is not needed when metadata is available")) + monkeypatch.setattr(versioning, "run_git_command", run_git_command) + monkeypatch.setattr(versioning, "get_config", get_config) + + assert versioning.get_version() == PACKAGE_VERSION + run_git_command.assert_not_called() + get_config.assert_not_called() + + +def test_get_version_falls_back_to_declared_version_when_metadata_is_missing(monkeypatch, tmp_path: Path): + package_root = tmp_path / "source-distribution" + package_root.mkdir() + monkeypatch.setattr(versioning, "get_config_path", lambda: package_root / "pyproject.toml") + monkeypatch.setattr(versioning, "_is_source_checkout", lambda _: False) + + def missing_distribution(name: str) -> str: + raise versioning.metadata.PackageNotFoundError(name) + + monkeypatch.setattr(versioning.metadata, "version", missing_distribution) + monkeypatch.setattr(versioning, "get_config", _version_config) + + assert versioning.get_version() == PACKAGE_VERSION + + +def test_get_version_anchors_source_checkout_git_commands(monkeypatch, tmp_path: Path): + package_root = tmp_path / "rocrate-validator" + package_root.mkdir() + monkeypatch.setattr(versioning, "get_config_path", lambda: package_root / "pyproject.toml") + monkeypatch.setattr(versioning, "_is_source_checkout", lambda _: True) + monkeypatch.setattr(versioning.metadata, "version", lambda _: pytest.fail("Source checkouts must use Git")) + monkeypatch.setattr(versioning, "get_config", _version_config) + + outputs = iter(["abc1234", "", PACKAGE_VERSION, "7", ""]) + calls: list[tuple[list[str], Path | None]] = [] + + def run_git_command(command: list[str], cwd: Path | None = None) -> str: + calls.append((command, cwd)) + return next(outputs) + + monkeypatch.setattr(versioning, "run_git_command", run_git_command) + + assert versioning.get_version() == f"{PACKAGE_VERSION}_abc1234+7" + assert len(calls) == 5 + assert all(cwd == package_root for _, cwd in calls) + + +@pytest.mark.skipif(shutil.which("git") is None, reason="Git is required for the consumer-repository regression") +def test_source_import_ignores_consumer_repository_tag(tmp_path: Path): + consumer = tmp_path / "consumer" + consumer.mkdir() + _run_git(consumer, "init", "--quiet") + _run_git( + consumer, + "-c", + "user.name=Issue 198 Test", + "-c", + "user.email=issue-198@example.invalid", + "-c", + "commit.gpgSign=false", + "commit", + "--allow-empty", + "--quiet", + "-m", + "consumer release", + ) + _run_git(consumer, "tag", "0.3.1") + + environment = os.environ.copy() + environment["PYTHONPATH"] = str(Path(__file__).resolve().parents[2]) + result = subprocess.run( + [sys.executable, "-c", "import rocrate_validator; print(rocrate_validator.__version__)"], + cwd=consumer, + env=environment, + check=False, + capture_output=True, + text=True, + ) + + output = result.stdout + result.stderr + assert result.returncode == 0, output + assert "0.3.1" not in output + assert "different from the last tag" not in output diff --git a/tests/unit/test_warm_start.py b/tests/unit/test_warm_start.py new file mode 100644 index 000000000..6f5750e11 --- /dev/null +++ b/tests/unit/test_warm_start.py @@ -0,0 +1,719 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import copy +import json +import shutil +import threading +import time +from pathlib import Path +from urllib.parse import urljoin + +import pytest +from rdflib import Graph, Namespace, URIRef +from rdflib.compare import isomorphic + +from rocrate_validator.errors import ValidationExecutionError +from rocrate_validator.models import URI, Profile, Requirement, ValidationContext, ValidationSettings, Validator +from rocrate_validator.models import validation as validation_module +from rocrate_validator.requirements.shacl.checks import SHACLCheck +from rocrate_validator.requirements.shacl.models import ShapesRegistry +from rocrate_validator.requirements.shacl.validator import SHACLValidationContext +from rocrate_validator.services import validate_metadata_as_dict +from tests.ro_crates import ValidROC + + +def _settings(crate: Path) -> ValidationSettings: + return ValidationSettings( + rocrate_uri=URI(crate), + extra_profiles_path=Path("tests/data/profiles/fake"), + profile_identifier="c", + offline=True, + ) + + +def test_repeated_validation_reuses_prepared_profiles(monkeypatch): + """A warm run must not reload profiles or rebuild their requirements.""" + calls = 0 + original = Profile.load_profiles + + def counted_load_profiles(cls, *args, **kwargs): + nonlocal calls + calls += 1 + return original(*args, **kwargs) + + monkeypatch.setattr(Profile, "load_profiles", classmethod(counted_load_profiles)) + validator = Validator(_settings(ValidROC().wrroc_paper)) + + cold_result = validator.validate() + warm_result = validator.validate() + + assert calls == 1 + assert cold_result.passed() == warm_result.passed() + assert cold_result.statistics.total_checks == warm_result.statistics.total_checks + assert cold_result.statistics.profiles == warm_result.statistics.profiles + + +def test_warm_run_does_not_reparse_shapes(monkeypatch): + calls = 0 + original = ShapesRegistry.load_shapes + + def counted_load_shapes(self, *args, **kwargs): + nonlocal calls + calls += 1 + return original(self, *args, **kwargs) + + monkeypatch.setattr(ShapesRegistry, "load_shapes", counted_load_shapes) + validator = Validator(_settings(ValidROC().wrroc_paper)) + + validator.validate() + cold_calls = calls + validator.validate() + + assert cold_calls > 0 + assert calls == cold_calls + + +def test_prepared_profiles_are_reused_across_crate_public_ids(monkeypatch): + """Different crate bases share one canonically prepared profile plan.""" + calls = 0 + original = Profile.load_profiles + + def counted_load_profiles(cls, *args, **kwargs): + nonlocal calls + calls += 1 + return original(*args, **kwargs) + + monkeypatch.setattr(Profile, "load_profiles", classmethod(counted_load_profiles)) + original_uri = URI(ValidROC().wrroc_paper) + settings = _settings(ValidROC().wrroc_paper) + validator = Validator(settings) + + first_result = validator.validate() + second_result = validator.validate(ValidROC().wrroc_paper_long_date) + + assert calls == 1 + assert settings.rocrate_uri == original_uri + assert first_result.rocrate_uri != second_result.rocrate_uri + shacl = Namespace("http://www.w3.org/ns/shacl#") + first_target = next( + ShapesRegistry.get_instance(first_result.context.profiles[-1]).shapes_graph.objects(predicate=shacl.targetNode) + ) + second_target = next( + ShapesRegistry.get_instance(second_result.context.profiles[-1]).shapes_graph.objects(predicate=shacl.targetNode) + ) + assert first_result.context.prepared_validation_plan is second_result.context.prepared_validation_plan + assert first_target == second_target + assert str(first_target).startswith(first_result.context.prepared_profile_base) + + +def test_ontology_rebasing_only_changes_lexically_relative_iris(tmp_path): + profiles_path = tmp_path / "profiles" + shutil.copytree(Path("tests/data/profiles/fake"), profiles_path) + (profiles_path / "c" / "ontology.ttl").write_text( + """ + PREFIX rel: <../> + PREFIX ex: + + rel:RelativeSubject ex:link rel:RelativeObject . + + ex:link . + + ex:link . + """, + encoding="utf-8", + ) + validator = Validator( + ValidationSettings( + rocrate_uri=URI(ValidROC().wrroc_paper), + profiles_path=profiles_path, + profile_identifier="c", + offline=True, + ) + ) + + result = validator.validate() + shacl_context = SHACLValidationContext.get_instance(result.context) + ontology_graph = shacl_context.ontology_graph + ex = Namespace("http://example.org/") + + assert ( + URIRef(urljoin(result.context.publicID, "../RelativeSubject")), + ex.link, + URIRef(urljoin(result.context.publicID, "../RelativeObject")), + ) in ontology_graph + assert ( + URIRef("https://github.com/crs4/rocrate-validator/prepared/crate/AbsoluteSubject"), + ex.link, + URIRef("https://github.com/crs4/rocrate-validator/prepared/crate/AbsoluteObject"), + ) in ontology_graph + assert ( + URIRef("https://example.invalid/external/AbsoluteSubject"), + ex.link, + URIRef("https://example.invalid/external/AbsoluteObject"), + ) in ontology_graph + + +def test_cross_base_reuse_keeps_each_crates_data_and_public_output_isolated(tmp_path, monkeypatch): + profile_loads = 0 + shape_loads = 0 + original_profile_load = Profile.load_profiles + original_shape_load = ShapesRegistry.load_shapes + + def counted_profile_load(cls, *args, **kwargs): + nonlocal profile_loads + profile_loads += 1 + return original_profile_load(*args, **kwargs) + + def counted_shape_load(self, *args, **kwargs): + nonlocal shape_loads + shape_loads += 1 + return original_shape_load(self, *args, **kwargs) + + monkeypatch.setattr(Profile, "load_profiles", classmethod(counted_profile_load)) + monkeypatch.setattr(ShapesRegistry, "load_shapes", counted_shape_load) + + context = { + "@vocab": "http://schema.org/", + "about": {"@type": "@id"}, + } + valid_metadata = { + "@context": context, + "@graph": [ + {"@id": "ro-crate-metadata.json", "@type": "CreativeWork", "about": {"@id": "./"}}, + {"@id": "./", "@type": "Dataset"}, + ], + } + invalid_metadata = copy.deepcopy(valid_metadata) + invalid_metadata["@graph"][0].pop("@type") + crate_a = tmp_path / "crate-a" + crate_b = tmp_path / "crate-b" + for crate, metadata in ((crate_a, valid_metadata), (crate_b, invalid_metadata)): + crate.mkdir() + (crate / "ro-crate-metadata.json").write_text(json.dumps(metadata), encoding="utf-8") + + validator = Validator(_settings(crate_a)) + valid_result = validator.validate() + invalid_result = validator.validate(crate_b) + + assert valid_result.passed() + assert not invalid_result.passed() + assert valid_result.context.publicID != invalid_result.context.publicID + assert valid_result.context.prepared_validation_plan is invalid_result.context.prepared_validation_plan + assert profile_loads == 1 + assert shape_loads > 0 + assert all(issue.violatingEntity == "./ro-crate-metadata.json" for issue in invalid_result.get_issues()) + + +def test_parent_relative_shape_target_is_rebased_for_validation(tmp_path): + """A parsed ``../`` target must follow each crate base after preparation.""" + profiles_path = tmp_path / "profiles" + shutil.copytree(Path("tests/data/profiles/fake"), profiles_path) + shape_path = profiles_path / "c" / "shape_c.ttl" + shape_path.write_text( + """ + @prefix sh: . + @prefix schema: . + + <../ShapeC> a sh:NodeShape ; + sh:targetNode <../target> ; + sh:property [ sh:path schema:name ; sh:minCount 1 ; sh:message "target must have a name" ] . + """, + encoding="utf-8", + ) + crate = tmp_path / "crate" + crate.mkdir() + metadata = { + "@context": {"@vocab": "http://schema.org/", "about": {"@type": "@id"}}, + "@graph": [ + {"@id": "ro-crate-metadata.json", "@type": "CreativeWork", "about": {"@id": "./"}}, + {"@id": "./", "@type": "Dataset"}, + {"@id": "../target", "@type": "Dataset"}, + ], + } + (crate / "ro-crate-metadata.json").write_text(json.dumps(metadata), encoding="utf-8") + settings = ValidationSettings( + rocrate_uri=URI(crate), + profiles_path=profiles_path, + profile_identifier="c", + offline=True, + ) + validator = Validator(settings) + + for valid in (True, False): + if valid: + metadata["@graph"][2]["name"] = "Present" + else: + metadata["@graph"][2].pop("name") + (crate / "ro-crate-metadata.json").write_text(json.dumps(metadata), encoding="utf-8") + result = validator.validate() + + assert result.passed() is valid + if not valid: + assert any(issue.message == "target must have a name" for issue in result.get_issues()) + assert result.get_issues()[0].violatingEntity == (crate.parent / "target").as_uri() + + +@pytest.mark.parametrize("explicit_target", [False, True], ids=["implicit", "explicit"]) +@pytest.mark.parametrize("class_type", ["rdfs:Class", "owl:Class", "rel:MetaClass"]) +@pytest.mark.parametrize("canonical_instance", [False, True], ids=["real-only", "canonical-decoy"]) +def test_structural_shape_identifier_is_rebased_as_target_class( + tmp_path, explicit_target, class_type, canonical_instance +): + """Class targets must follow the crate without selecting canonical-base instances.""" + profiles_path = tmp_path / "profiles" + shutil.copytree(Path("tests/data/profiles/fake"), profiles_path) + shape_path = profiles_path / "c" / "shape_c.ttl" + shape_path.write_text( + """ + @prefix rel: <./> . + @prefix rdfs: . + @prefix owl: . + @prefix schema: . + @prefix sh: . + + rel:Dataset a sh:NodeShape, CLASS_TYPE ; + TARGET + sh:property [ sh:path schema:name ; sh:minCount 1 ; + sh:message "class-target check must run" ] . + """.replace("CLASS_TYPE", class_type).replace( + "TARGET", "sh:targetClass rel:Dataset ;" if explicit_target else "" + ), + encoding="utf-8", + ) + inherited_shape = profiles_path / "a" / "shape_a.ttl" + inherited_shape.write_text( + inherited_shape.read_text(encoding="utf-8") + "\n " + " .\n", + encoding="utf-8", + ) + first_plan = None + for crate_name in ("crate-a", "crate-b"): + crate = tmp_path / crate_name + crate.mkdir() + metadata = { + "@context": {"@vocab": "http://schema.org/", "about": {"@type": "@id"}}, + "@graph": [ + {"@id": "ro-crate-metadata.json", "@type": "CreativeWork", "about": {"@id": "./"}}, + {"@id": "./", "@type": "Dataset"}, + {"@id": "./item", "@type": f"{crate.as_uri()}/Dataset", "name": "Present"}, + ], + } + if canonical_instance: + # This unrelated instance must not become an extra implicit target. + metadata["@graph"].append( + { + "@id": "./unrelated", + "@type": "https://github.com/crs4/rocrate-validator/prepared/crate/Dataset", + } + ) + (crate / "ro-crate-metadata.json").write_text(json.dumps(metadata), encoding="utf-8") + if first_plan is None: + validator = Validator( + ValidationSettings( + rocrate_uri=URI(crate), + profiles_path=profiles_path, + profile_identifier="c", + offline=True, + ) + ) + for valid in (True, False): + if not valid: + metadata["@graph"][2].pop("name") + (crate / "ro-crate-metadata.json").write_text(json.dumps(metadata), encoding="utf-8") + result = validator.validate(crate) + assert result.passed() is valid + if not valid: + assert [issue.message for issue in result.get_issues()] == ["class-target check must run"] + assert result.get_issues()[0].violatingEntity == "./item" + plan = result.context.prepared_validation_plan + registry = ShapesRegistry.get_instance(result.context.profiles[-1]) + if first_plan is None: + first_plan = plan + prepared_shapes = registry.shapes_graph + assert plan is first_plan + assert isomorphic(prepared_shapes, registry.shapes_graph) + + +def test_absolute_canonical_class_target_is_not_rebased(tmp_path): + """An absolute class IRI on the preparation authority must retain its identity.""" + profiles_path = tmp_path / "profiles" + shutil.copytree(Path("tests/data/profiles/fake"), profiles_path) + absolute_class = "https://github.com/crs4/rocrate-validator/prepared/crate/Dataset" + shape_path = profiles_path / "c" / "shape_c.ttl" + shape_path.write_text( + f""" + @prefix rdfs: . + @prefix schema: . + @prefix sh: . + + <{absolute_class}> a sh:NodeShape, rdfs:Class ; + sh:property [ sh:path schema:name ; sh:minCount 1 ; + sh:message "absolute-class check must run" ] . + """, + encoding="utf-8", + ) + crate = tmp_path / "crate" + crate.mkdir() + metadata = { + "@context": {"@vocab": "http://schema.org/", "about": {"@type": "@id"}}, + "@graph": [ + {"@id": "ro-crate-metadata.json", "@type": "CreativeWork", "about": {"@id": "./"}}, + {"@id": "./", "@type": "Dataset"}, + {"@id": "./item", "@type": absolute_class, "name": "Present"}, + ], + } + (crate / "ro-crate-metadata.json").write_text(json.dumps(metadata), encoding="utf-8") + validator = Validator( + ValidationSettings( + rocrate_uri=URI(crate), + profiles_path=profiles_path, + profile_identifier="c", + offline=True, + ) + ) + + valid_result = validator.validate() + assert valid_result.passed() + metadata["@graph"][2].pop("name") + (crate / "ro-crate-metadata.json").write_text(json.dumps(metadata), encoding="utf-8") + invalid_result = validator.validate() + + assert not invalid_result.passed() + assert any(issue.message == "absolute-class check must run" for issue in invalid_result.get_issues()) + assert invalid_result.get_issues()[0].violatingEntity == "./item" + + +def test_per_call_metadata_does_not_mutate_validator_settings(): + crate = ValidROC().wrroc_paper + with (crate / "ro-crate-metadata.json").open(encoding="utf-8") as stream: + metadata = json.load(stream) + metadata["@context"] = { + "@vocab": "http://schema.org/", + "about": {"@type": "@id"}, + "conformsTo": {"@type": "@id"}, + } + settings = _settings(crate) + validator = Validator(settings) + + result = validator.validate(metadata_dict=metadata) + + assert result.passed() + assert settings.metadata_dict is None + assert not settings.metadata_only + + +def test_distinct_explicit_metadata_bases_keep_separate_plans(): + crate = ValidROC().wrroc_paper + with (crate / "ro-crate-metadata.json").open(encoding="utf-8") as stream: + first_metadata = json.load(stream) + first_metadata["@context"] = { + "@base": "https://example.org/crate-a/", + "@vocab": "http://schema.org/", + "about": {"@type": "@id"}, + "conformsTo": {"@type": "@id"}, + } + second_metadata = copy.deepcopy(first_metadata) + second_metadata["@context"]["@base"] = "https://example.org/crate-b/" + validator = Validator(_settings(crate)) + + first = validator.validate(metadata_dict=first_metadata) + second = validator.validate(metadata_dict=second_metadata) + + assert first.context.effective_ontology_base == "https://example.org/crate-a/" + assert second.context.effective_ontology_base == "https://example.org/crate-b/" + assert first.context.prepared_validation_plan is not second.context.prepared_validation_plan + + +def test_cached_plan_resolves_bare_profile_token_for_each_per_call_validation(tmp_path): + """A cache hit must not defer every SHACL check for a per-call bare token.""" + crate = tmp_path / "crate" + crate.mkdir() + metadata = { + "@context": {"@vocab": "http://schema.org/", "about": {"@type": "@id"}}, + "@graph": [ + # The selected versioned profile requires this descriptor to have + # an RDF type. Leave it out so each call must report a violation. + {"@id": "ro-crate-metadata.json", "about": {"@id": "./"}}, + {"@id": "./", "@type": "Dataset"}, + ], + } + (crate / "ro-crate-metadata.json").write_text(json.dumps(metadata), encoding="utf-8") + settings = ValidationSettings( + rocrate_uri=URI(crate), + profiles_path=Path("tests/data/profiles/fake_versioned_profiles"), + profile_identifier="c", + offline=True, + ) + validator = Validator(settings) + + first_result = validator.validate(crate) + second_result = validator.validate(crate) + + assert not first_result.passed() + assert not second_result.passed() + assert len(first_result.executed_checks) > 0 + assert len(second_result.executed_checks) == len(first_result.executed_checks) + assert [issue.message for issue in second_result.get_issues()] == [ + issue.message for issue in first_result.get_issues() + ] + + +def test_metadata_service_does_not_mutate_caller_settings(): + crate = ValidROC().wrroc_paper + with (crate / "ro-crate-metadata.json").open(encoding="utf-8") as stream: + metadata = json.load(stream) + metadata["@context"] = { + "@vocab": "http://schema.org/", + "about": {"@type": "@id"}, + "conformsTo": {"@type": "@id"}, + } + settings = _settings(crate) + + result = validate_metadata_as_dict(metadata, settings) + + assert result.passed() + assert settings.metadata_dict is None + assert not settings.metadata_only + + +def test_per_call_inputs_are_mutually_exclusive(): + validator = Validator(_settings(ValidROC().wrroc_paper)) + + with pytest.raises(ValueError, match="mutually exclusive"): + validator.validate(ValidROC().wrroc_paper, metadata_dict={"@graph": []}) + + +def test_requirement_selection_survives_a_per_call_base_change(monkeypatch): + settings = _settings(ValidROC().wrroc_paper) + validator = Validator(settings) + source_context = ValidationContext(validator, settings) + selected = source_context.profiles[-1].requirements[0] + executed = [] + + def record_requirement(requirement, context): + executed.append(requirement.identifier) + return True + + monkeypatch.setattr(Requirement, "_do_validate_", record_requirement) + + validator.validate_requirements( + [selected], + include_dependencies=False, + rocrate_uri=ValidROC().wrroc_paper_long_date, + ) + + assert executed == [selected.identifier] + + +def test_clear_prepared_profiles_forces_reload(monkeypatch): + calls = 0 + original = Profile.load_profiles + + def counted_load_profiles(cls, *args, **kwargs): + nonlocal calls + calls += 1 + return original(*args, **kwargs) + + monkeypatch.setattr(Profile, "load_profiles", classmethod(counted_load_profiles)) + validator = Validator(_settings(ValidROC().wrroc_paper)) + + validator.validate() + validator.clear_prepared_profiles() + validator.validate() + + assert calls == 2 + + +def test_profile_detection_and_validation_share_catalog(monkeypatch): + calls = 0 + original = Profile.load_profiles + + def counted_load_profiles(cls, *args, **kwargs): + nonlocal calls + calls += 1 + return original(*args, **kwargs) + + monkeypatch.setattr(Profile, "load_profiles", classmethod(counted_load_profiles)) + validator = Validator(_settings(ValidROC().wrroc_paper)) + + detected = validator.detect_rocrate_profiles() + result = validator.validate() + + assert detected + assert result.passed() + assert calls == 1 + + +def test_prepare_can_eagerly_warm_and_refresh_profiles(monkeypatch): + calls = 0 + original = Profile.load_profiles + + def counted_load_profiles(cls, *args, **kwargs): + nonlocal calls + calls += 1 + return original(*args, **kwargs) + + monkeypatch.setattr(Profile, "load_profiles", classmethod(counted_load_profiles)) + validator = Validator(_settings(ValidROC().wrroc_paper)) + + validator.prepare() + validator.validate() + assert calls == 1 + + validator.prepare(refresh=True) + validator.validate() + assert calls == 2 + + +def test_inherited_ontologies_are_prepared_once(monkeypatch): + ontology_parses = 0 + original_parse = validation_module.parse_turtle_with_relative_iris + graph_parse = Graph.parse + + def counted_parse(source, *args, **kwargs): + nonlocal ontology_parses + if source is not None and str(source).endswith("ontology.ttl"): + ontology_parses += 1 + return original_parse(source, *args, **kwargs) + + monkeypatch.setattr(validation_module, "parse_turtle_with_relative_iris", counted_parse) + settings = ValidationSettings( + rocrate_uri=URI(ValidROC().wrroc_paper), + profile_identifier="isa-ro-crate", + offline=True, + ) + validator = Validator(settings) + + first_context = ValidationContext(validator, settings) + first_plan = first_context.prepared_validation_plan + second_context = ValidationContext(validator, settings) + second_plan = second_context.prepared_validation_plan + + expected = Graph() + for profile in first_plan.profiles: + ontology_path = profile.path / "ontology.ttl" + if ontology_path.exists(): + graph_parse(expected, ontology_path, format="ttl", publicID=first_context.prepared_ontology_base) + + assert ontology_parses == 2 + assert first_plan is second_plan + assert isomorphic(first_plan.ontology_graph, expected) + + +def test_failed_run_does_not_contaminate_next_run(monkeypatch): + validator = Validator(_settings(ValidROC().wrroc_paper)) + original_execute_check = SHACLCheck.execute_check + + def fail_validation(*args, **kwargs): + raise ValidationExecutionError("synthetic engine failure") + + monkeypatch.setattr(SHACLCheck, "execute_check", fail_validation) + with pytest.raises(ValidationExecutionError, match="synthetic engine failure"): + validator.validate() + + monkeypatch.setattr(SHACLCheck, "execute_check", original_execute_check) + assert validator.validate().passed() + + +def test_per_run_graph_mutations_do_not_reach_the_next_run(): + validator = Validator(_settings(ValidROC().wrroc_paper)) + marker = ( + URIRef("https://example.org/run"), + URIRef("https://example.org/has"), + URIRef("https://example.org/private-marker"), + ) + + first = validator.validate() + first_shacl_context = SHACLValidationContext.get_instance(first.context) + first_shacl_context.ontology_graph.add(marker) + first_shacl_context.shapes_registry._shapes_graph.add(marker) + + second = validator.validate() + second_shacl_context = SHACLValidationContext.get_instance(second.context) + + assert marker not in second_shacl_context.ontology_graph + assert marker not in second_shacl_context.shapes_registry.shapes_graph + assert marker not in second.context.prepared_validation_plan.ontology_graph + + +def test_cold_and_warm_runs_report_equivalent_violations(): + crate = ValidROC().wrroc_paper + with (crate / "ro-crate-metadata.json").open(encoding="utf-8") as stream: + metadata = json.load(stream) + metadata["@context"] = { + "@vocab": "http://schema.org/", + "about": {"@type": "@id"}, + "conformsTo": {"@type": "@id"}, + } + settings = ValidationSettings( # type: ignore[call-arg] + metadata_dict=metadata, + profiles_path=Path("tests/data/profiles/sparql_test"), + profile_identifier="sparql-test", + offline=True, + ) + validator = Validator(settings) + + cold = validator.validate() + warm = validator.validate() + + def issue_signature(result): + return [ + ( + issue.check.identifier, + issue.message, + issue.violatingEntity, + issue.violatingProperty, + issue.violatingPropertyValue, + ) + for issue in result.get_issues() + ] + + assert issue_signature(cold) + assert issue_signature(cold) == issue_signature(warm) + assert cold.statistics.total_checks == warm.statistics.total_checks + assert cold.statistics.check_count_by_severity == warm.statistics.check_count_by_severity + + +def test_validator_serializes_overlapping_runs(monkeypatch): + validator = Validator(_settings(ValidROC().wrroc_paper)) + active = 0 + max_active = 0 + state_lock = threading.Lock() + original = validator.__do_validate_locked__ + + def tracked(*args, **kwargs): + nonlocal active, max_active + with state_lock: + active += 1 + max_active = max(max_active, active) + time.sleep(0.05) + try: + return original(*args, **kwargs) + finally: + with state_lock: + active -= 1 + + monkeypatch.setattr(validator, "__do_validate_locked__", tracked) + results = [] + threads = [threading.Thread(target=lambda: results.append(validator.validate())) for _ in range(2)] + + for thread in threads: + thread.start() + for thread in threads: + thread.join() + + assert max_active == 1 + assert len(results) == 2 + assert all(result.passed() for result in results) diff --git a/tests/unit/transformers/test_validation_candidate_marker.py b/tests/unit/transformers/test_validation_candidate_marker.py new file mode 100644 index 000000000..a7df129bc --- /dev/null +++ b/tests/unit/transformers/test_validation_candidate_marker.py @@ -0,0 +1,317 @@ +# Copyright (c) 2024-2026 CRS4 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +from rdflib import RDF, BNode, Dataset, Graph, Literal, Namespace, URIRef +from rdflib.namespace import SH + +from rocrate_validator.requirements.shacl import SHACLValidator +from rocrate_validator.requirements.shacl.errors import SHACLValidationError +from rocrate_validator.requirements.shacl.transformers import ( + REQUIRES_GRAPH_TRANSFORMER_PREDICATE, + prepare_data_graph, +) +from rocrate_validator.transformers.validation_candidate_marker import ( + VALIDATION_CANDIDATE_PREDICATE, + VALIDATION_CANDIDATE_TRANSFORMER, +) + +SCHEMA = Namespace("http://schema.org/") +CSVW = Namespace("http://www.w3.org/ns/csvw#") +DCTERMS = Namespace("http://purl.org/dc/terms/") +EXAMPLE = Namespace("https://example.org/") + + +def _is_validation_candidate(graph: Graph, iri: URIRef) -> bool: + return (iri, VALIDATION_CANDIDATE_PREDICATE, Literal(True)) in graph + + +@pytest.mark.parametrize( + "predicate", + [ + RDF.type, + SCHEMA.propertyID, + SCHEMA.additionalType, + CSVW.propertyUrl, + SCHEMA.inDefinedTermSet, + SCHEMA.encodingFormat, + DCTERMS.conformsTo, + ], +) +def test_reference_objects_are_marked_cited_only(predicate: URIRef): + data_graph = Graph() + term = EXAMPLE["term"] + data_graph.add((EXAMPLE.entity, predicate, term)) + + validation_graph = prepare_data_graph(data_graph) + + assert not _is_validation_candidate(validation_graph, term) + + +@pytest.mark.parametrize("predicate", [SCHEMA.author, SCHEMA.hasPart]) +def test_claimed_objects_remain_in_validation_scope(predicate: URIRef): + data_graph = Graph() + data_graph.add((EXAMPLE.entity, predicate, EXAMPLE.claimed)) + + validation_graph = prepare_data_graph(data_graph) + + assert _is_validation_candidate(validation_graph, EXAMPLE.claimed) + + +def test_described_subject_remains_in_validation_scope(): + data_graph = Graph() + data_graph.add((EXAMPLE.described, RDF.type, SCHEMA.Person)) + data_graph.add((EXAMPLE.described, SCHEMA.name, Literal("A person"))) + + validation_graph = prepare_data_graph(data_graph) + + assert _is_validation_candidate(validation_graph, EXAMPLE.described) + assert not _is_validation_candidate(validation_graph, SCHEMA.Person) + + +@pytest.mark.parametrize("subject", [EXAMPLE.type_only, BNode("type-only")]) +def test_type_only_subject_remains_in_validation_scope(subject: URIRef | BNode): + data_graph = Graph() + data_graph.add((subject, RDF.type, SCHEMA.Person)) + + validation_graph = prepare_data_graph(data_graph) + + assert _is_validation_candidate(validation_graph, subject) + assert not _is_validation_candidate(validation_graph, SCHEMA.Person) + + +def test_claimed_vocabulary_term_is_a_validation_candidate(): + data_graph = Graph() + data_graph.add((EXAMPLE.entity, SCHEMA.author, EXAMPLE.ontology_term)) + + validation_graph = prepare_data_graph(data_graph) + + assert _is_validation_candidate(validation_graph, EXAMPLE.ontology_term) + + +def test_node_absent_from_original_graph_is_not_a_validation_candidate(): + data_graph = Graph() + + validation_graph = prepare_data_graph(data_graph) + + assert not _is_validation_candidate(validation_graph, EXAMPLE.ontology_term) + + +def test_preparation_does_not_mutate_input_graphs(): + data_graph = Graph() + term = EXAMPLE["term"] + data_graph.add((EXAMPLE.entity, SCHEMA.propertyID, term)) + original_data_triples = set(data_graph) + + validation_graph = prepare_data_graph(data_graph) + + assert validation_graph is not data_graph + assert set(data_graph) == original_data_triples + assert len(validation_graph) > len(data_graph) + + +@pytest.mark.parametrize("serialized", [False, True]) +def test_shacl_validator_prepares_graph_before_validation(serialized: bool): + shape = EXAMPLE.marker_shape + target = BNode() + property_shape = BNode() + shapes_graph = Graph() + shapes_graph.add((shape, RDF.type, SH.NodeShape)) + shapes_graph.add((shape, REQUIRES_GRAPH_TRANSFORMER_PREDICATE, VALIDATION_CANDIDATE_TRANSFORMER)) + shapes_graph.add((shape, SH.target, target)) + shapes_graph.add( + ( + target, + SH.select, + Literal( + f"SELECT ?this WHERE {{ ?this a <{SCHEMA.Person}> . " + f"FILTER EXISTS {{ ?this <{VALIDATION_CANDIDATE_PREDICATE}> true }} }}" + ), + ) + ) + shapes_graph.add((target, RDF.type, SH.SPARQLTarget)) + shapes_graph.add((shape, SH.property, property_shape)) + shapes_graph.add((property_shape, SH.path, SCHEMA.name)) + shapes_graph.add((property_shape, SH.minCount, Literal(1))) + shapes_graph.add((property_shape, SH.message, Literal("A person needs a name"))) + + person = EXAMPLE.person + data_graph = Graph() + data_graph.add((EXAMPLE.entity, SCHEMA.author, person)) + data_graph.add((person, RDF.type, SCHEMA.Person)) + original_triples = set(data_graph) + + source = data_graph.serialize(format="turtle").encode() if serialized else data_graph + result = SHACLValidator(shapes_graph=shapes_graph).validate(source) + + assert not result.conforms + assert any(violation.focusNode == person for violation in result.violations) + assert set(data_graph) == original_triples + + +def test_serialized_shapes_can_request_graph_transformers(): + shape = EXAMPLE.marker_shape + target = BNode() + property_shape = BNode() + shapes_graph = Graph() + shapes_graph.add((shape, RDF.type, SH.NodeShape)) + shapes_graph.add((shape, REQUIRES_GRAPH_TRANSFORMER_PREDICATE, VALIDATION_CANDIDATE_TRANSFORMER)) + shapes_graph.add((shape, SH.target, target)) + shapes_graph.add((target, RDF.type, SH.SPARQLTarget)) + shapes_graph.add( + ( + target, + SH.select, + Literal(f"SELECT ?this WHERE {{ ?this <{VALIDATION_CANDIDATE_PREDICATE}> true }}"), + ) + ) + shapes_graph.add((shape, SH.property, property_shape)) + shapes_graph.add((property_shape, SH.path, SCHEMA.name)) + shapes_graph.add((property_shape, SH.minCount, Literal(1))) + data_graph = Graph() + data_graph.add((EXAMPLE.entity, SCHEMA.author, EXAMPLE.person)) + + result = SHACLValidator(shapes_graph=shapes_graph.serialize(format="turtle").encode()).validate( + data_graph, + shacl_graph_format="turtle", + ) + + assert not result.conforms + assert any(violation.focusNode in {EXAMPLE.entity, EXAMPLE.person} for violation in result.violations) + + +def _closed_shape() -> Graph: + shape = EXAMPLE.closed_shape + property_shape = BNode() + shapes_graph = Graph() + shapes_graph.add((shape, RDF.type, SH.NodeShape)) + shapes_graph.add((shape, SH.targetNode, EXAMPLE.entity)) + shapes_graph.add((shape, SH.closed, Literal(True))) + shapes_graph.add((shape, SH.property, property_shape)) + shapes_graph.add((property_shape, SH.path, SCHEMA.name)) + return shapes_graph + + +def test_unrequested_transformers_do_not_change_closed_shapes(): + data_graph = Graph() + data_graph.add((EXAMPLE.entity, SCHEMA.name, Literal("Entity"))) + + result = SHACLValidator(shapes_graph=_closed_shape()).validate(data_graph) + + assert result.conforms + + +def test_transformer_annotations_are_ignored_by_closed_shapes(): + data_graph = Graph() + data_graph.add((EXAMPLE.entity, SCHEMA.name, Literal("Entity"))) + shapes_graph = _closed_shape() + shapes_graph.add( + ( + EXAMPLE.closed_shape, + REQUIRES_GRAPH_TRANSFORMER_PREDICATE, + VALIDATION_CANDIDATE_TRANSFORMER, + ) + ) + + result = SHACLValidator(shapes_graph=shapes_graph).validate(data_graph) + + assert result.conforms + + +def test_dataset_named_graphs_are_preserved_during_preparation(): + data_graph = Dataset() + data_graph.graph(EXAMPLE.named_graph).add((EXAMPLE.entity, SCHEMA.name, Literal("Entity"))) + original_quads = set(data_graph.quads((None, None, None, None))) + shape = EXAMPLE.dataset_shape + property_shape = BNode() + shapes_graph = Graph() + shapes_graph.add((shape, RDF.type, SH.NodeShape)) + shapes_graph.add((shape, SH.targetSubjectsOf, SCHEMA.name)) + shapes_graph.add((shape, SH.property, property_shape)) + shapes_graph.add((property_shape, SH.path, SCHEMA.description)) + shapes_graph.add((property_shape, SH.minCount, Literal(1))) + shapes_graph.add((shape, REQUIRES_GRAPH_TRANSFORMER_PREDICATE, VALIDATION_CANDIDATE_TRANSFORMER)) + + result = SHACLValidator(shapes_graph=shapes_graph).validate(data_graph) + + assert not result.conforms + assert any(violation.focusNode == EXAMPLE.entity for violation in result.violations) + assert set(data_graph.quads((None, None, None, None))) == original_quads + assert not data_graph.default_union + + +def test_serialized_parse_errors_keep_shacl_error_contract(): + shape = EXAMPLE.marker_shape + shapes_graph = Graph() + shapes_graph.add((shape, RDF.type, SH.NodeShape)) + shapes_graph.add((shape, REQUIRES_GRAPH_TRANSFORMER_PREDICATE, VALIDATION_CANDIDATE_TRANSFORMER)) + + with pytest.raises(SHACLValidationError, match="BadSyntax") as exc_info: + SHACLValidator(shapes_graph=shapes_graph).validate( + b"@prefix broken", + data_graph_format="turtle", + ) + + assert exc_info.value.__cause__ is not None + + +def test_inplace_remains_effective_without_requested_transformers(): + shape = EXAMPLE.rule_shape + rule = BNode() + shapes_graph = Graph() + shapes_graph.add((shape, RDF.type, SH.NodeShape)) + shapes_graph.add((shape, SH.targetNode, EXAMPLE.entity)) + shapes_graph.add((shape, SH.rule, rule)) + shapes_graph.add((rule, RDF.type, SH.TripleRule)) + shapes_graph.add((rule, SH.subject, SH.this)) + shapes_graph.add((rule, SH.predicate, EXAMPLE.generated)) + shapes_graph.add((rule, SH.object, Literal("yes"))) + data_graph = Graph() + + SHACLValidator(shapes_graph=shapes_graph).validate(data_graph, inplace=True) + + assert (EXAMPLE.entity, EXAMPLE.generated, Literal("yes")) in data_graph + + +def test_inplace_is_rejected_for_transformer_dependent_shapes(): + shape = EXAMPLE.marker_shape + shapes_graph = Graph() + shapes_graph.add((shape, RDF.type, SH.NodeShape)) + shapes_graph.add((shape, REQUIRES_GRAPH_TRANSFORMER_PREDICATE, VALIDATION_CANDIDATE_TRANSFORMER)) + + with pytest.raises(ValueError, match="inplace=True"): + SHACLValidator(shapes_graph=shapes_graph).validate(Graph(), inplace=True) + + +def test_sparql_mode_is_rejected_for_transformer_dependent_shapes(): + shape = EXAMPLE.marker_shape + shapes_graph = Graph() + shapes_graph.add((shape, RDF.type, SH.NodeShape)) + shapes_graph.add((shape, REQUIRES_GRAPH_TRANSFORMER_PREDICATE, VALIDATION_CANDIDATE_TRANSFORMER)) + + with pytest.raises(ValueError, match="sparql_mode"): + SHACLValidator(shapes_graph=shapes_graph).validate( + "https://example.org/sparql", + sparql_mode=True, + ) + + +def test_unknown_requested_transformer_fails_validation(): + shape = EXAMPLE.marker_shape + shapes_graph = Graph() + shapes_graph.add((shape, RDF.type, SH.NodeShape)) + shapes_graph.add((shape, REQUIRES_GRAPH_TRANSFORMER_PREDICATE, EXAMPLE.unknown_transformer)) + + with pytest.raises(SHACLValidationError, match="Unknown graph transformer"): + SHACLValidator(shapes_graph=shapes_graph).validate(Graph())