@hestia-earth/data-validation 0.37.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.coveragerc +14 -0
- package/.dockerignore +19 -0
- package/.eslintignore +17 -0
- package/.eslintrc.js +11 -0
- package/.flake8 +5 -0
- package/.gitlab/issue_templates/new validation.md +82 -0
- package/.gitlab-ci.yml +216 -0
- package/.readthedocs.yml +24 -0
- package/CODEOWNERS +11 -0
- package/Dockerfile +13 -0
- package/LICENSE +21 -0
- package/MANIFEST.in +2 -0
- package/bin/hestia-validate-data +80 -0
- package/build_mocking.py +14 -0
- package/commitlint.config.js +1 -0
- package/docs/Makefile +20 -0
- package/docs/_static/styles.css +4 -0
- package/docs/_templates/custom-class-template.rst +34 -0
- package/docs/_templates/custom-module-template.rst +66 -0
- package/docs/_templates/layout.html +4 -0
- package/docs/conf.py +74 -0
- package/docs/index.rst +42 -0
- package/docs/make.bat +35 -0
- package/docs/requirements.txt +13 -0
- package/envs/.develop.env +1 -0
- package/envs/.master.env +1 -0
- package/guide-assets/.gitkeep +0 -0
- package/hestia_earth/validation/README.md +5 -0
- package/hestia_earth/validation/__init__.py +32 -0
- package/hestia_earth/validation/distribution.py +22 -0
- package/hestia_earth/validation/gee.py +162 -0
- package/hestia_earth/validation/log.py +44 -0
- package/hestia_earth/validation/models.py +141 -0
- package/hestia_earth/validation/preload_requests.py +61 -0
- package/hestia_earth/validation/terms.py +88 -0
- package/hestia_earth/validation/utils.py +444 -0
- package/hestia_earth/validation/validators/__init__.py +141 -0
- package/hestia_earth/validation/validators/aggregated_cycle.py +32 -0
- package/hestia_earth/validation/validators/aggregated_shared.py +37 -0
- package/hestia_earth/validation/validators/animal.py +88 -0
- package/hestia_earth/validation/validators/completeness.py +252 -0
- package/hestia_earth/validation/validators/cycle.py +1123 -0
- package/hestia_earth/validation/validators/distribution.py +86 -0
- package/hestia_earth/validation/validators/emission.py +109 -0
- package/hestia_earth/validation/validators/impact_assessment.py +138 -0
- package/hestia_earth/validation/validators/indicator.py +154 -0
- package/hestia_earth/validation/validators/infrastructure.py +25 -0
- package/hestia_earth/validation/validators/input.py +268 -0
- package/hestia_earth/validation/validators/management.py +131 -0
- package/hestia_earth/validation/validators/measurement.py +368 -0
- package/hestia_earth/validation/validators/organisation.py +43 -0
- package/hestia_earth/validation/validators/practice.py +590 -0
- package/hestia_earth/validation/validators/product.py +263 -0
- package/hestia_earth/validation/validators/property.py +266 -0
- package/hestia_earth/validation/validators/shared.py +940 -0
- package/hestia_earth/validation/validators/site.py +312 -0
- package/hestia_earth/validation/validators/source.py +20 -0
- package/hestia_earth/validation/validators/transformation.py +250 -0
- package/hestia_earth/validation/version.py +1 -0
- package/layer/build.sh +34 -0
- package/layer/deploy.sh +18 -0
- package/package.json +59 -0
- package/release.sh +11 -0
- package/requirements-ci.txt +6 -0
- package/requirements-test.txt +4 -0
- package/requirements.txt +2 -0
- package/run-docker-test.sh +7 -0
- package/run-docker.sh +9 -0
- package/run.py +99 -0
- package/scripts/build_docs.py +283 -0
- package/scripts/build_validation_list.py +160 -0
- package/scripts/guide-create-branch.sh +15 -0
- package/scripts/update-package-version.js +28 -0
- package/search-results.json +384 -0
- package/setup.cfg +2 -0
- package/setup.py +35 -0
- package/src/index.ts +1 -0
- package/src/validations.ts +22 -0
- package/src/version.ts +1 -0
- package/tests/Dockerfile +13 -0
- package/tests/__init__.py +3 -0
- package/tests/fixtures/aggregated/cycle/inputs-impactAssessment/invalid-no-impactAssessment.json +64 -0
- package/tests/fixtures/aggregated/cycle/inputs-impactAssessment/invalid-world.json +69 -0
- package/tests/fixtures/aggregated/cycle/inputs-impactAssessment/valid.json +69 -0
- package/tests/fixtures/animal/duplicated-input-cycle/invalid.json +98 -0
- package/tests/fixtures/animal/duplicated-input-cycle/valid.json +91 -0
- package/tests/fixtures/animal/pregnancyRateTotal/invalid.json +49 -0
- package/tests/fixtures/animal/pregnancyRateTotal/valid.json +60 -0
- package/tests/fixtures/animal/required/invalid.json +59 -0
- package/tests/fixtures/animal/required/valid.json +72 -0
- package/tests/fixtures/completeness/all-values/warning.json +22 -0
- package/tests/fixtures/completeness/animalPopulation/invalid.json +58 -0
- package/tests/fixtures/completeness/animalPopulation/valid-animals.json +71 -0
- package/tests/fixtures/completeness/animalPopulation/valid-incomplete.json +58 -0
- package/tests/fixtures/completeness/animalPopulation/valid-no-liveAnimals.json +37 -0
- package/tests/fixtures/completeness/blank-nodes/agri-food processor-invalid.json +52 -0
- package/tests/fixtures/completeness/blank-nodes/invalid.json +124 -0
- package/tests/fixtures/completeness/blank-nodes/valid.json +128 -0
- package/tests/fixtures/completeness/cropland/site.json +16 -0
- package/tests/fixtures/completeness/cropland/valid.json +22 -0
- package/tests/fixtures/completeness/cropland/warning.json +22 -0
- package/tests/fixtures/completeness/freshForage/error-animals.json +63 -0
- package/tests/fixtures/completeness/freshForage/error-products.json +65 -0
- package/tests/fixtures/completeness/freshForage/valid-animal-inputs.json +63 -0
- package/tests/fixtures/completeness/freshForage/valid-animals.json +63 -0
- package/tests/fixtures/completeness/freshForage/valid-not-grazing-liveAnimal.json +55 -0
- package/tests/fixtures/completeness/freshForage/valid-not-liveAnimal.json +47 -0
- package/tests/fixtures/completeness/freshForage/valid-products.json +68 -0
- package/tests/fixtures/completeness/ingredient/invalid-agri-food-processor.json +37 -0
- package/tests/fixtures/completeness/ingredient/invalid.json +49 -0
- package/tests/fixtures/completeness/ingredient/valid-agri-food-processor-complete.json +49 -0
- package/tests/fixtures/completeness/ingredient/valid-agri-food-processor-incomplete.json +37 -0
- package/tests/fixtures/completeness/ingredient/valid.json +49 -0
- package/tests/fixtures/completeness/material/error.json +49 -0
- package/tests/fixtures/completeness/material/valid-fuel-material.json +60 -0
- package/tests/fixtures/completeness/material/valid-incomplete.json +36 -0
- package/tests/fixtures/completeness/material/valid-no-fuel.json +36 -0
- package/tests/fixtures/completeness/valid.json +22 -0
- package/tests/fixtures/cycle/aboveGroundCropResidue/invalid.json +76 -0
- package/tests/fixtures/cycle/aboveGroundCropResidue/valid.json +76 -0
- package/tests/fixtures/cycle/aggregated-valid.json +102 -0
- package/tests/fixtures/cycle/coverCrop/invalid.json +64 -0
- package/tests/fixtures/cycle/coverCrop/valid-not-coverCrop.json +54 -0
- package/tests/fixtures/cycle/coverCrop/valid.json +64 -0
- package/tests/fixtures/cycle/cropResidue/complete/invalid.json +56 -0
- package/tests/fixtures/cycle/cropResidue/complete/valid.json +82 -0
- package/tests/fixtures/cycle/cropResidue/incomplete/invalid.json +42 -0
- package/tests/fixtures/cycle/cropResidue/incomplete/valid.json +56 -0
- package/tests/fixtures/cycle/dates/invalid-emissions.json +70 -0
- package/tests/fixtures/cycle/liveAnimal-animalProduct-mapping/invalid.json +63 -0
- package/tests/fixtures/cycle/liveAnimal-animalProduct-mapping/valid.json +63 -0
- package/tests/fixtures/cycle/maximumCycleDuration/invalid-dates-year-only.json +48 -0
- package/tests/fixtures/cycle/maximumCycleDuration/invalid-dates.json +48 -0
- package/tests/fixtures/cycle/maximumCycleDuration/invalid.json +48 -0
- package/tests/fixtures/cycle/maximumCycleDuration/valid-dates-year-only.json +48 -0
- package/tests/fixtures/cycle/maximumCycleDuration/valid-dates.json +48 -0
- package/tests/fixtures/cycle/maximumCycleDuration/valid.json +48 -0
- package/tests/fixtures/cycle/otherSites/cycleDuration/invalid.json +52 -0
- package/tests/fixtures/cycle/otherSites/cycleDuration/valid-no-siteDuration.json +40 -0
- package/tests/fixtures/cycle/otherSites/cycleDuration/valid.json +52 -0
- package/tests/fixtures/cycle/practices/stockingDensityPermanentPastureAverage/invalid.json +56 -0
- package/tests/fixtures/cycle/practices/stockingDensityPermanentPastureAverage/valid.json +65 -0
- package/tests/fixtures/cycle/primary-product-as-input/invalid.json +59 -0
- package/tests/fixtures/cycle/primary-product-as-input/valid.json +48 -0
- package/tests/fixtures/cycle/product-linked-ia/cycle.json +66 -0
- package/tests/fixtures/cycle/product-linked-ia/invalid-multiple.json +58 -0
- package/tests/fixtures/cycle/product-linked-ia/valid.json +57 -0
- package/tests/fixtures/cycle/products/animals/invalid.json +69 -0
- package/tests/fixtures/cycle/products/animals/valid.json +58 -0
- package/tests/fixtures/cycle/riceGrainInHuskFlooded-minimumCycleDuration/invalid.json +53 -0
- package/tests/fixtures/cycle/riceGrainInHuskFlooded-minimumCycleDuration/valid.json +53 -0
- package/tests/fixtures/cycle/siteDuration/crop/invalid.json +53 -0
- package/tests/fixtures/cycle/siteDuration/crop/valid-different-duration.json +53 -0
- package/tests/fixtures/cycle/siteDuration/crop/valid-same-duration.json +53 -0
- package/tests/fixtures/cycle/siteDuration/invalid.json +41 -0
- package/tests/fixtures/cycle/siteDuration/valid-no-siteDuration.json +40 -0
- package/tests/fixtures/cycle/siteDuration/valid-otherSites.json +48 -0
- package/tests/fixtures/cycle/siteDuration/valid.json +45 -0
- package/tests/fixtures/cycle/substrate/required/invalid.json +50 -0
- package/tests/fixtures/cycle/substrate/required/valid.json +60 -0
- package/tests/fixtures/cycle/valid.json +343 -0
- package/tests/fixtures/emission/linked-terms/inputs/invalid.json +78 -0
- package/tests/fixtures/emission/linked-terms/inputs/valid.json +106 -0
- package/tests/fixtures/emission/linked-terms/transformation/error.json +104 -0
- package/tests/fixtures/emission/linked-terms/transformation/valid.json +107 -0
- package/tests/fixtures/emission/linked-terms/transformation/warning.json +76 -0
- package/tests/fixtures/emission/methodTier-background/invalid.json +60 -0
- package/tests/fixtures/emission/methodTier-background/valid.json +60 -0
- package/tests/fixtures/emission/not-relevant/invalid.json +71 -0
- package/tests/fixtures/emission/not-relevant/valid.json +95 -0
- package/tests/fixtures/emission/not-relevant-methodTier/invalid.json +70 -0
- package/tests/fixtures/emission/not-relevant-methodTier/valid.json +95 -0
- package/tests/fixtures/impactAssessment/aggregated-valid.json +43 -0
- package/tests/fixtures/impactAssessment/cycle-contains-product/invalid.json +34 -0
- package/tests/fixtures/impactAssessment/cycle-contains-product/valid.json +34 -0
- package/tests/fixtures/impactAssessment/cycle-endDate/invalid.json +26 -0
- package/tests/fixtures/impactAssessment/cycle-endDate/valid.json +26 -0
- package/tests/fixtures/impactAssessment/valid.json +93 -0
- package/tests/fixtures/indicator/characterisedIndicator-methodModel/invalid.json +52 -0
- package/tests/fixtures/indicator/characterisedIndicator-methodModel/valid.json +52 -0
- package/tests/fixtures/indicator/ionisingCompounds/invalid.json +23 -0
- package/tests/fixtures/indicator/ionisingCompounds/valid.json +23 -0
- package/tests/fixtures/indicator/landTransformation/invalid-grouped.json +257 -0
- package/tests/fixtures/indicator/landTransformation/invalid.json +100 -0
- package/tests/fixtures/indicator/landTransformation/valid-grouped-full.json +507 -0
- package/tests/fixtures/indicator/landTransformation/valid-grouped.json +507 -0
- package/tests/fixtures/indicator/landTransformation/valid.json +100 -0
- package/tests/fixtures/infrastructure/lifespan/invalid.json +26 -0
- package/tests/fixtures/infrastructure/lifespan/valid.json +45 -0
- package/tests/fixtures/input/animalFeed-fate/invalid.json +103 -0
- package/tests/fixtures/input/animalFeed-fate/valid.json +90 -0
- package/tests/fixtures/input/country/invalid.json +64 -0
- package/tests/fixtures/input/country/valid.json +64 -0
- package/tests/fixtures/input/distribution/animalHousing.json +103 -0
- package/tests/fixtures/input/distribution/complete/invalid.json +177 -0
- package/tests/fixtures/input/distribution/complete/valid.json +163 -0
- package/tests/fixtures/input/distribution/incomplete/valid.json +139 -0
- package/tests/fixtures/input/impactAssessment/invalid.json +99 -0
- package/tests/fixtures/input/impactAssessment/valid.json +89 -0
- package/tests/fixtures/input/input-as-product/invalid.json +57 -0
- package/tests/fixtures/input/input-as-product/valid.json +59 -0
- package/tests/fixtures/input/mustIncludeId/invalid.json +13 -0
- package/tests/fixtures/input/mustIncludeId/valid-multiple-ids.json +31 -0
- package/tests/fixtures/input/mustIncludeId/valid.json +22 -0
- package/tests/fixtures/input/saplings/invalid.json +58 -0
- package/tests/fixtures/input/saplings/valid-no-saplings.json +58 -0
- package/tests/fixtures/input/saplings/valid-not-plantation.json +58 -0
- package/tests/fixtures/input/saplings/valid.json +58 -0
- package/tests/fixtures/integration/distribution/product-yield-invalid.json +54 -0
- package/tests/fixtures/management/cycle-overlap/cycles.json +39 -0
- package/tests/fixtures/management/cycle-overlap/invalid.json +26 -0
- package/tests/fixtures/management/cycle-overlap/valid.json +26 -0
- package/tests/fixtures/management/exists/invalid.json +13 -0
- package/tests/fixtures/management/exists/valid.json +25 -0
- package/tests/fixtures/management/fallow-dates/invalid.json +24 -0
- package/tests/fixtures/management/fallow-dates/valid.json +24 -0
- package/tests/fixtures/management/termType/invalid-cropland.json +35 -0
- package/tests/fixtures/management/termType/invalid-permanent-pasture.json +25 -0
- package/tests/fixtures/management/termType/valid-cropland.json +55 -0
- package/tests/fixtures/management/termType/valid-no-management.json +13 -0
- package/tests/fixtures/management/termType/valid-permanent-pasture.json +35 -0
- package/tests/fixtures/measurement/depths/invalid.json +44 -0
- package/tests/fixtures/measurement/depths/valid.json +50 -0
- package/tests/fixtures/measurement/models/valid.json +33 -0
- package/tests/fixtures/measurement/models/warning-no-value.json +30 -0
- package/tests/fixtures/measurement/models/warning.json +33 -0
- package/tests/fixtures/measurement/pond-measurements/invalid.json +11 -0
- package/tests/fixtures/measurement/pond-measurements/valid.json +23 -0
- package/tests/fixtures/measurement/required-depths/error.json +71 -0
- package/tests/fixtures/measurement/required-depths/valid.json +126 -0
- package/tests/fixtures/measurement/required-depths/warning.json +29 -0
- package/tests/fixtures/measurement/soilTexture/missing-texture-value.json +227 -0
- package/tests/fixtures/measurement/soilTexture/percent-invalid.json +110 -0
- package/tests/fixtures/measurement/soilTexture/percent-missing-value.json +43 -0
- package/tests/fixtures/measurement/soilTexture/percent-valid.json +110 -0
- package/tests/fixtures/measurement/startDate-endDate-required/invalid.json +32 -0
- package/tests/fixtures/measurement/startDate-endDate-required/valid.json +46 -0
- package/tests/fixtures/measurement/unique/invalid.json +28 -0
- package/tests/fixtures/measurement/unique/valid.json +16 -0
- package/tests/fixtures/measurement/value-length/invalid.json +46 -0
- package/tests/fixtures/measurement/value-length/valid.json +44 -0
- package/tests/fixtures/measurement/water-salinity/invalid.json +33 -0
- package/tests/fixtures/measurement/water-salinity/valid-brakish.json +40 -0
- package/tests/fixtures/measurement/water-salinity/valid.json +33 -0
- package/tests/fixtures/organisation/valid.json +26 -0
- package/tests/fixtures/practice/croppingDuration/riceGrainInHuskFlooded/invalid.json +63 -0
- package/tests/fixtures/practice/croppingDuration/riceGrainInHuskFlooded/valid.json +63 -0
- package/tests/fixtures/practice/defaultValue/invalid.json +12 -0
- package/tests/fixtures/practice/defaultValue/valid.json +15 -0
- package/tests/fixtures/practice/excretaManagement/invalid.json +50 -0
- package/tests/fixtures/practice/excretaManagement/valid.json +60 -0
- package/tests/fixtures/practice/irrigated-complete/invalid.json +47 -0
- package/tests/fixtures/practice/irrigated-complete/valid-incomplete.json +47 -0
- package/tests/fixtures/practice/irrigated-complete/valid.json +60 -0
- package/tests/fixtures/practice/landCover-products/invalid.json +58 -0
- package/tests/fixtures/practice/landCover-products/valid-coverCrop.json +69 -0
- package/tests/fixtures/practice/landCover-products/valid.json +58 -0
- package/tests/fixtures/practice/liveAnimal-system/invalid.json +58 -0
- package/tests/fixtures/practice/liveAnimal-system/valid.json +69 -0
- package/tests/fixtures/practice/longFallowDuration/invalid.json +20 -0
- package/tests/fixtures/practice/longFallowDuration/valid.json +20 -0
- package/tests/fixtures/practice/noTillage/invalid.json +23 -0
- package/tests/fixtures/practice/noTillage/valid-value-not-100.json +23 -0
- package/tests/fixtures/practice/noTillage/valid.json +21 -0
- package/tests/fixtures/practice/pastureGrass/key-termType/invalid.json +16 -0
- package/tests/fixtures/practice/pastureGrass/key-termType/valid.json +16 -0
- package/tests/fixtures/practice/pastureGrass/key-value/invalid-numbers.json +67 -0
- package/tests/fixtures/practice/pastureGrass/key-value/invalid.json +67 -0
- package/tests/fixtures/practice/pastureGrass/key-value/valid.json +67 -0
- package/tests/fixtures/practice/pastureGrass/permanent-pasture/invalid.json +37 -0
- package/tests/fixtures/practice/pastureGrass/permanent-pasture/valid.json +47 -0
- package/tests/fixtures/practice/primaryPercent/invalid.json +49 -0
- package/tests/fixtures/practice/primaryPercent/valid.json +49 -0
- package/tests/fixtures/practice/processingOperation/invalid-no-primary.json +48 -0
- package/tests/fixtures/practice/processingOperation/invalid.json +49 -0
- package/tests/fixtures/practice/processingOperation/valid-cropland.json +37 -0
- package/tests/fixtures/practice/processingOperation/valid.json +49 -0
- package/tests/fixtures/practice/productivePhasePermanentCrops/invalid.json +48 -0
- package/tests/fixtures/practice/productivePhasePermanentCrops/valid-0-value.json +58 -0
- package/tests/fixtures/practice/productivePhasePermanentCrops/valid-no-value.json +47 -0
- package/tests/fixtures/practice/productivePhasePermanentCrops/valid.json +48 -0
- package/tests/fixtures/practice/site-management/invalid.json +75 -0
- package/tests/fixtures/practice/site-management/valid.json +75 -0
- package/tests/fixtures/practice/tillage-siteType/valid.json +51 -0
- package/tests/fixtures/practice/tillage-siteType/warning.json +42 -0
- package/tests/fixtures/practice/tillage-values/invalid-fullTillage.json +61 -0
- package/tests/fixtures/practice/tillage-values/invalid-noTillage.json +61 -0
- package/tests/fixtures/practice/tillage-values/valid.json +61 -0
- package/tests/fixtures/practice/waterRegime/rice/invalid.json +59 -0
- package/tests/fixtures/practice/waterRegime/rice/valid-0-value.json +59 -0
- package/tests/fixtures/practice/waterRegime/rice/valid.json +58 -0
- package/tests/fixtures/product/economicValueShare/invalid.json +31 -0
- package/tests/fixtures/product/economicValueShare/valid.json +22 -0
- package/tests/fixtures/product/excreta/invalid.json +62 -0
- package/tests/fixtures/product/excreta/valid.json +62 -0
- package/tests/fixtures/product/excreta/warning.json +53 -0
- package/tests/fixtures/product/excreta/with-system/invalid.json +79 -0
- package/tests/fixtures/product/excreta/with-system/valid.json +88 -0
- package/tests/fixtures/product/excreta/with-system/warning.json +70 -0
- package/tests/fixtures/product/fu_ha/invalid.json +49 -0
- package/tests/fixtures/product/fu_ha/valid.json +49 -0
- package/tests/fixtures/product/primary/invalid.json +22 -0
- package/tests/fixtures/product/primary/valid.json +22 -0
- package/tests/fixtures/product/value/valid.json +26 -0
- package/tests/fixtures/product/value/value-0/error.json +40 -0
- package/tests/fixtures/product/value/value-empty/warning.json +23 -0
- package/tests/fixtures/product/yield/invalid.json +54 -0
- package/tests/fixtures/product/yield/no-value.json +75 -0
- package/tests/fixtures/product/yield/valid.json +54 -0
- package/tests/fixtures/property/default-value/valid-allowed-exception.json +61 -0
- package/tests/fixtures/property/default-value/valid.json +61 -0
- package/tests/fixtures/property/default-value/warning.json +61 -0
- package/tests/fixtures/property/termType/invalid.json +60 -0
- package/tests/fixtures/property/termType/valid.json +60 -0
- package/tests/fixtures/property/value-min-max/invalid.json +77 -0
- package/tests/fixtures/property/value-min-max/valid-skip-maximum.json +57 -0
- package/tests/fixtures/property/value-min-max/valid.json +78 -0
- package/tests/fixtures/property/valueType/invalid.json +79 -0
- package/tests/fixtures/property/valueType/valid.json +79 -0
- package/tests/fixtures/property/volatileSolidsContent/invalid.json +99 -0
- package/tests/fixtures/property/volatileSolidsContent/valid.json +99 -0
- package/tests/fixtures/shared/coordinates/invalid.json +18 -0
- package/tests/fixtures/shared/coordinates/valid.json +18 -0
- package/tests/fixtures/shared/data-duplicates/valid.json +113 -0
- package/tests/fixtures/shared/data-duplicates/warning.json +172 -0
- package/tests/fixtures/shared/duplicated-term-units/invalid-animalProduct.json +61 -0
- package/tests/fixtures/shared/duplicated-term-units/invalid-organicFertiliser.json +61 -0
- package/tests/fixtures/shared/duplicated-term-units/valid.json +49 -0
- package/tests/fixtures/shared/list-country-region/invalid.json +54 -0
- package/tests/fixtures/shared/list-country-region/valid.json +54 -0
- package/tests/fixtures/shared/list-percent-value/invalid.json +49 -0
- package/tests/fixtures/shared/list-percent-value/valid.json +52 -0
- package/tests/fixtures/shared/list-valueType/invalid.json +49 -0
- package/tests/fixtures/shared/list-valueType/valid.json +49 -0
- package/tests/fixtures/shared/list-values-sum-100/management/with-properties/valid.json +91 -0
- package/tests/fixtures/shared/list-values-sum-100/measurements/missing-soil.json +46 -0
- package/tests/fixtures/shared/list-values-sum-100/measurements/no-depth-high-value.json +63 -0
- package/tests/fixtures/shared/list-values-sum-100/measurements/no-depth-valid.json +40 -0
- package/tests/fixtures/shared/list-values-sum-100/measurements/with-depth-high-value.json +71 -0
- package/tests/fixtures/shared/list-values-sum-100/practices/total-100.json +61 -0
- package/tests/fixtures/shared/list-values-sum-100/practices/total-110.json +61 -0
- package/tests/fixtures/shared/list-values-sum-100/practices/total-90.json +61 -0
- package/tests/fixtures/shared/min-max/value-above.json +31 -0
- package/tests/fixtures/shared/min-max/value-below.json +31 -0
- package/tests/fixtures/shared/min-max/value-valid.json +45 -0
- package/tests/fixtures/shared/model/emissions/invalid.json +102 -0
- package/tests/fixtures/shared/model/emissions/valid-variable-tolerance.json +180 -0
- package/tests/fixtures/shared/model/emissions/valid.json +102 -0
- package/tests/fixtures/shared/model/impacts/invalid.json +75 -0
- package/tests/fixtures/shared/model/impacts/valid.json +75 -0
- package/tests/fixtures/shared/model/inputs/valid-no-value.json +84 -0
- package/tests/fixtures/shared/model/inputs/valid.json +87 -0
- package/tests/fixtures/shared/model/inputs/warning.json +87 -0
- package/tests/fixtures/shared/model/products/valid-no-value.json +88 -0
- package/tests/fixtures/shared/model/products/valid.json +91 -0
- package/tests/fixtures/shared/model/products/warning.json +91 -0
- package/tests/fixtures/shared/otherModel/invalid.json +69 -0
- package/tests/fixtures/shared/otherModel/valid.json +70 -0
- package/tests/fixtures/shared/properties-duplicate-values/invalid.json +61 -0
- package/tests/fixtures/shared/properties-duplicate-values/valid.json +57 -0
- package/tests/fixtures/shared/properties-same-length/invalid.json +62 -0
- package/tests/fixtures/shared/properties-same-length/valid.json +52 -0
- package/tests/fixtures/shared/unit-percent/invalid.json +34 -0
- package/tests/fixtures/shared/unit-percent/valid.json +60 -0
- package/tests/fixtures/shared/unit-percent/warning.json +52 -0
- package/tests/fixtures/site/cycles-linked-ia/invalid.json +129 -0
- package/tests/fixtures/site/cycles-linked-ia/valid.json +129 -0
- package/tests/fixtures/site/valid.json +138 -0
- package/tests/fixtures/source/valid.json +19 -0
- package/tests/fixtures/transformation/excretaManagement/invalid.json +47 -0
- package/tests/fixtures/transformation/excretaManagement/valid.json +59 -0
- package/tests/fixtures/transformation/inputs-products/invalid.json +43 -0
- package/tests/fixtures/transformation/inputs-products/valid.json +43 -0
- package/tests/fixtures/transformation/linked-emission/invalid.json +101 -0
- package/tests/fixtures/transformation/linked-emission/valid.json +107 -0
- package/tests/fixtures/transformation/previousTransformationId/invalid-no-previous.json +127 -0
- package/tests/fixtures/transformation/previousTransformationId/invalid-previous-input.json +100 -0
- package/tests/fixtures/transformation/previousTransformationId/invalid-product-input.json +106 -0
- package/tests/fixtures/transformation/previousTransformationId/invalid-wrong-order.json +136 -0
- package/tests/fixtures/transformation/previousTransformationId/valid.json +171 -0
- package/tests/integration/__init__.py +0 -0
- package/tests/integration/test_product.py +17 -0
- package/tests/test_gee.py +10 -0
- package/tests/test_utils.py +36 -0
- package/tests/test_validation.py +11 -0
- package/tests/utils.py +28 -0
- package/tests/validators/__init__.py +0 -0
- package/tests/validators/test_aggregated_cycle.py +44 -0
- package/tests/validators/test_aggregated_shared.py +63 -0
- package/tests/validators/test_animal.py +72 -0
- package/tests/validators/test_completeness.py +337 -0
- package/tests/validators/test_cycle.py +600 -0
- package/tests/validators/test_emission.py +170 -0
- package/tests/validators/test_impact_assessment.py +80 -0
- package/tests/validators/test_indicator.py +120 -0
- package/tests/validators/test_infrastructure.py +26 -0
- package/tests/validators/test_input.py +434 -0
- package/tests/validators/test_management.py +177 -0
- package/tests/validators/test_measurement.py +317 -0
- package/tests/validators/test_organisation.py +32 -0
- package/tests/validators/test_practice.py +490 -0
- package/tests/validators/test_product.py +291 -0
- package/tests/validators/test_property.py +143 -0
- package/tests/validators/test_shared.py +1139 -0
- package/tests/validators/test_site.py +151 -0
- package/tests/validators/test_source.py +15 -0
- package/tests/validators/test_transformation.py +151 -0
- package/tests/validators/test_validators.py +74 -0
- package/tsconfig.dist.json +9 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,940 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from functools import reduce
|
|
5
|
+
from typing import List
|
|
6
|
+
import re
|
|
7
|
+
from hestia_earth.schema import TermTermType, SiteSiteType
|
|
8
|
+
from hestia_earth.utils.api import download_hestia
|
|
9
|
+
from hestia_earth.utils.model import filter_list_term_type
|
|
10
|
+
from hestia_earth.utils.tools import (
|
|
11
|
+
flatten,
|
|
12
|
+
list_sum,
|
|
13
|
+
safe_parse_float,
|
|
14
|
+
safe_parse_date,
|
|
15
|
+
get_dict_key,
|
|
16
|
+
to_precision,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from hestia_earth.validation.gee import (
|
|
20
|
+
MAX_AREA_SIZE,
|
|
21
|
+
is_enabled as gee_is_enabled,
|
|
22
|
+
id_to_level,
|
|
23
|
+
get_region_id,
|
|
24
|
+
get_region_distance,
|
|
25
|
+
)
|
|
26
|
+
from hestia_earth.validation.models import (
|
|
27
|
+
is_enabled as models_is_enabled,
|
|
28
|
+
value_from_model,
|
|
29
|
+
method_tier_from_model,
|
|
30
|
+
run_model,
|
|
31
|
+
run_model_from_node,
|
|
32
|
+
)
|
|
33
|
+
from hestia_earth.validation.utils import (
|
|
34
|
+
update_error_path,
|
|
35
|
+
_filter_list_errors,
|
|
36
|
+
_next_error,
|
|
37
|
+
_value_average,
|
|
38
|
+
is_number,
|
|
39
|
+
match_value_type,
|
|
40
|
+
find_linked_node,
|
|
41
|
+
_is_before_today,
|
|
42
|
+
_list_except_item,
|
|
43
|
+
_dict_without_key,
|
|
44
|
+
hash_dict,
|
|
45
|
+
group_blank_nodes,
|
|
46
|
+
term_valueType,
|
|
47
|
+
get_lookup_value,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
_VALIDATE_PRIVATE_SOURCE = os.getenv("VALIDATE_PRIVATE_SOURCE", "true") == "true"
|
|
51
|
+
CROP_SITE_TYPE = [
|
|
52
|
+
SiteSiteType.CROPLAND.value,
|
|
53
|
+
SiteSiteType.GLASS_OR_HIGH_ACCESSIBLE_COVER.value,
|
|
54
|
+
]
|
|
55
|
+
OTHER_MODEL_ID = "otherModel"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def validate_properties_same_length(
|
|
59
|
+
node: dict, list_key: str, prop_key: str, prop_keys: list
|
|
60
|
+
):
|
|
61
|
+
def validate(values: tuple):
|
|
62
|
+
index, blank_node = values
|
|
63
|
+
value_len = len(blank_node.get(prop_key, ""))
|
|
64
|
+
invalid_prop_key = next(
|
|
65
|
+
(
|
|
66
|
+
key
|
|
67
|
+
for key in prop_keys
|
|
68
|
+
if blank_node.get(key) and len(blank_node.get(key)) != value_len
|
|
69
|
+
),
|
|
70
|
+
None,
|
|
71
|
+
)
|
|
72
|
+
return (
|
|
73
|
+
value_len == 0
|
|
74
|
+
or invalid_prop_key is None
|
|
75
|
+
or {
|
|
76
|
+
"level": "error",
|
|
77
|
+
"dataPath": f".{list_key}[{index}].{invalid_prop_key}",
|
|
78
|
+
"message": f"must have the same length as {prop_key}",
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return _filter_list_errors(
|
|
83
|
+
flatten(map(validate, enumerate(node.get(list_key, []))))
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def validate_date_lt_today(node: dict, key: str):
|
|
88
|
+
date = get_dict_key(node, key)
|
|
89
|
+
return (
|
|
90
|
+
date is None
|
|
91
|
+
or _is_before_today(date)
|
|
92
|
+
or {"level": "error", "dataPath": f".{key}", "message": "must be before today"}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def validate_list_date_lt_today(node: dict, list_key: str, node_keys: list):
|
|
97
|
+
def validate(values: tuple):
|
|
98
|
+
index, value = values
|
|
99
|
+
errors = list(
|
|
100
|
+
map(
|
|
101
|
+
lambda key: {"key": key, "error": validate_date_lt_today(value, key)},
|
|
102
|
+
node_keys,
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
return _filter_list_errors(
|
|
106
|
+
[
|
|
107
|
+
update_error_path(error["error"], list_key, index)
|
|
108
|
+
for error in errors
|
|
109
|
+
if error["error"] is not True
|
|
110
|
+
]
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return _filter_list_errors(
|
|
114
|
+
flatten(map(validate, enumerate(node.get(list_key, []))))
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def is_date_after(min_date: str, date: str, strict: bool = True):
|
|
119
|
+
return (
|
|
120
|
+
min_date is None
|
|
121
|
+
or date is None
|
|
122
|
+
or (len(min_date) <= 7 and len(date) <= 7 and date >= min_date)
|
|
123
|
+
or (date > min_date if strict else date >= min_date)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def is_date_equal(date1: str, date2: str, validate_year_only: bool = False):
|
|
128
|
+
date1 = safe_parse_date(date1)
|
|
129
|
+
date2 = safe_parse_date(date2)
|
|
130
|
+
return (
|
|
131
|
+
(date1.year == date2.year if validate_year_only else date1 == date2)
|
|
132
|
+
if all([date1, date2])
|
|
133
|
+
else False
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def validate_list_dates_after(
|
|
138
|
+
node: dict, node_key: str, list_key: str, list_key_fields: list
|
|
139
|
+
):
|
|
140
|
+
min_date = node.get(node_key)
|
|
141
|
+
|
|
142
|
+
def validate_field_list(blank_node: dict, index: int, field: str, field_index: int):
|
|
143
|
+
date = blank_node.get(field)[field_index]
|
|
144
|
+
return is_date_after(min_date, date, False) or {
|
|
145
|
+
"level": "warning",
|
|
146
|
+
"dataPath": f".{list_key}[{index}].{field}[{field_index}]",
|
|
147
|
+
"message": f"must be greater than {node.get('type', node.get('@type'))} {node_key}",
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
def validate_field(blank_node: dict, index: int, field: str):
|
|
151
|
+
date = blank_node.get(field)
|
|
152
|
+
return (
|
|
153
|
+
[
|
|
154
|
+
validate_field_list(blank_node, index, field, field_index)
|
|
155
|
+
for field_index in range(0, len(date))
|
|
156
|
+
]
|
|
157
|
+
if isinstance(date, list)
|
|
158
|
+
else (
|
|
159
|
+
is_date_after(min_date, date, False)
|
|
160
|
+
or {
|
|
161
|
+
"level": "warning",
|
|
162
|
+
"dataPath": f".{list_key}[{index}].{field}",
|
|
163
|
+
"message": f"must be greater than {node.get('type', node.get('@type'))} {node_key}",
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def validate(values: tuple):
|
|
169
|
+
index, blank_node = values
|
|
170
|
+
return _filter_list_errors(
|
|
171
|
+
flatten(
|
|
172
|
+
[validate_field(blank_node, index, field) for field in list_key_fields]
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return _filter_list_errors(map(validate, enumerate(node.get(list_key, []))))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def validate_dates(node: dict):
|
|
180
|
+
return is_date_after(node.get("startDate"), node.get("endDate"))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def validate_dates_format(node: dict):
|
|
184
|
+
return any(
|
|
185
|
+
[
|
|
186
|
+
not node.get("startDate"),
|
|
187
|
+
not node.get("endDate"),
|
|
188
|
+
len(node.get("startDate", "")) == len(node.get("endDate", "")),
|
|
189
|
+
]
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def validate_list_dates(node: dict, list_key: str):
|
|
194
|
+
def validate(values: tuple):
|
|
195
|
+
index, value = values
|
|
196
|
+
return validate_dates(value) or {
|
|
197
|
+
"level": "error",
|
|
198
|
+
"dataPath": f".{list_key}[{index}].endDate",
|
|
199
|
+
"message": "must be greater than startDate",
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return _filter_list_errors(map(validate, enumerate(node.get(list_key, []))))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def validate_list_dates_format(node: dict, list_key: str):
|
|
206
|
+
return validate_properties_same_length(node, list_key, "endDate", ["startDate"])
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def validate_list_dates_length(node: dict, list_key: str):
|
|
210
|
+
def validate(values: tuple):
|
|
211
|
+
index, blank_node = values
|
|
212
|
+
value = blank_node.get("value")
|
|
213
|
+
dates = blank_node.get("dates")
|
|
214
|
+
return (
|
|
215
|
+
value is None
|
|
216
|
+
or dates is None
|
|
217
|
+
or len(dates) == len(value)
|
|
218
|
+
or {
|
|
219
|
+
"level": "error",
|
|
220
|
+
"dataPath": f".{list_key}[{index}].dates",
|
|
221
|
+
"message": "must contain as many items as values",
|
|
222
|
+
"params": {"expected": len(value), "current": len(dates)},
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return _filter_list_errors(map(validate, enumerate(node.get(list_key, []))))
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def is_value_below(value1, value2):
|
|
230
|
+
compare_lists = isinstance(value1, list) and isinstance(value2, list)
|
|
231
|
+
return any([value1 is None, value2 is None]) or (
|
|
232
|
+
_is_list_value_below(value1, value2)
|
|
233
|
+
if compare_lists
|
|
234
|
+
else any(
|
|
235
|
+
[
|
|
236
|
+
# allow 1% of rounding error
|
|
237
|
+
value1 <= value2 * 1.01,
|
|
238
|
+
Decimal(str(value1)) <= Decimal(str(value2)),
|
|
239
|
+
]
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _is_list_value_below(list1: list, list2: list):
|
|
245
|
+
def compare_enum(index: int):
|
|
246
|
+
return is_value_below(list1[index], list2[index])
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
len(list1) != len(list2)
|
|
250
|
+
or next(
|
|
251
|
+
(x for x in list(map(compare_enum, range(len(list1)))) if x is not True),
|
|
252
|
+
True,
|
|
253
|
+
)
|
|
254
|
+
is True
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def validate_list_value_between_min_max(node: dict, list_key: str):
|
|
259
|
+
def validate(values: tuple):
|
|
260
|
+
index, blank_node = values
|
|
261
|
+
min = blank_node.get("min")
|
|
262
|
+
max = blank_node.get("max")
|
|
263
|
+
value = blank_node.get("value")
|
|
264
|
+
|
|
265
|
+
return all([is_value_below(value, max), is_value_below(min, value)]) or {
|
|
266
|
+
"level": "error",
|
|
267
|
+
"dataPath": f".{list_key}[{index}].value",
|
|
268
|
+
"message": "must be between min and max",
|
|
269
|
+
"params": {"min": min, "max": max},
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return _next_error(list(map(validate, enumerate(node.get(list_key, [])))))
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def validate_list_min_below_max(node: dict, list_key: str):
|
|
276
|
+
def validate(values: tuple):
|
|
277
|
+
index, blank_node = values
|
|
278
|
+
min = blank_node.get("min")
|
|
279
|
+
max = blank_node.get("max")
|
|
280
|
+
return is_value_below(min, max) or {
|
|
281
|
+
"level": "error",
|
|
282
|
+
"dataPath": f".{list_key}[{index}].max",
|
|
283
|
+
"message": "must be greater than min",
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return _next_error(list(map(validate, enumerate(node.get(list_key, [])))))
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _value_range_error(value: int, minimum: int, maximum: int):
|
|
290
|
+
return (
|
|
291
|
+
"minimum"
|
|
292
|
+
if minimum is not None and not is_value_below(minimum, value)
|
|
293
|
+
else (
|
|
294
|
+
"maximum"
|
|
295
|
+
if maximum is not None and not is_value_below(value, maximum)
|
|
296
|
+
else False
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def validate_list_min_max_lookup(
|
|
302
|
+
node: dict, list_key: list, list_key_field="value", skip_max_ids: List[str] = []
|
|
303
|
+
):
|
|
304
|
+
def validate(values: tuple):
|
|
305
|
+
index, blank_node = values
|
|
306
|
+
term = blank_node.get("term", {})
|
|
307
|
+
term_id = term.get("@id")
|
|
308
|
+
mininum = safe_parse_float(get_lookup_value(term, "minimum"), None)
|
|
309
|
+
maximum = (
|
|
310
|
+
None
|
|
311
|
+
if term_id in skip_max_ids
|
|
312
|
+
else safe_parse_float(get_lookup_value(term, "maximum"), None)
|
|
313
|
+
)
|
|
314
|
+
value = _value_average(blank_node, None, list_key_field)
|
|
315
|
+
error = (
|
|
316
|
+
_value_range_error(value, mininum, maximum) if value is not None else False
|
|
317
|
+
)
|
|
318
|
+
return error is False or (
|
|
319
|
+
{
|
|
320
|
+
"level": "error",
|
|
321
|
+
"dataPath": f".{list_key}[{index}].{list_key_field}",
|
|
322
|
+
"message": f"should be above {mininum}",
|
|
323
|
+
}
|
|
324
|
+
if error == "minimum"
|
|
325
|
+
else {
|
|
326
|
+
"level": "error",
|
|
327
|
+
"dataPath": f".{list_key}[{index}].{list_key_field}",
|
|
328
|
+
"message": f"should be below {maximum}",
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
return _filter_list_errors(map(validate, enumerate(node.get(list_key, []))))
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def validate_nodes_duplicates(node: dict, node_by_hash: dict):
|
|
336
|
+
node_without_id = _dict_without_key(node, "id")
|
|
337
|
+
key = hash_dict(node_without_id)
|
|
338
|
+
duplicates = _list_except_item(node_by_hash.get(key, []), node)
|
|
339
|
+
return (
|
|
340
|
+
[
|
|
341
|
+
next(
|
|
342
|
+
(
|
|
343
|
+
{
|
|
344
|
+
"level": "warning",
|
|
345
|
+
"dataPath": "",
|
|
346
|
+
"message": f"might be a duplicate of the {dup.get('type')} with id {dup.get('id')}",
|
|
347
|
+
}
|
|
348
|
+
for dup in duplicates
|
|
349
|
+
),
|
|
350
|
+
True,
|
|
351
|
+
)
|
|
352
|
+
]
|
|
353
|
+
if len(duplicates) > 0
|
|
354
|
+
else []
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def validate_list_duplicate_values(node: dict, list_key: str, prop: str, value: str):
|
|
359
|
+
values = node.get(list_key, [])
|
|
360
|
+
duplicates = list(filter(lambda v: get_dict_key(v, prop) == value, values))
|
|
361
|
+
return len(duplicates) < 2 or {
|
|
362
|
+
"level": "error",
|
|
363
|
+
"dataPath": f".{list_key}[{values.index(duplicates[1])}].{prop}",
|
|
364
|
+
"message": f"must have only one entry with the same {prop} = {value}",
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def validate_list_term_percent(node: dict, list_key: str):
|
|
369
|
+
def soft_validate(index: int, value):
|
|
370
|
+
return (is_number(value) and 0 < value and value <= 1) and {
|
|
371
|
+
"level": "warning",
|
|
372
|
+
"dataPath": f".{list_key}[{index}].value",
|
|
373
|
+
"message": "may be between 0 and 100",
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
def hard_validate(index: int, value):
|
|
377
|
+
return (is_number(value) and 0 <= value and value <= 100) or {
|
|
378
|
+
"level": "error",
|
|
379
|
+
"dataPath": f".{list_key}[{index}].value",
|
|
380
|
+
"message": "should be between 0 and 100 (percentage)",
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
def validate(values: tuple):
|
|
384
|
+
index, blank_node = values
|
|
385
|
+
units = blank_node.get("term", {}).get("units", "")
|
|
386
|
+
value = (
|
|
387
|
+
_value_average(blank_node, blank_node.get("value"))
|
|
388
|
+
if units == "%"
|
|
389
|
+
else None
|
|
390
|
+
)
|
|
391
|
+
is_empty = value is None or (isinstance(value, list) and len(value) == 0)
|
|
392
|
+
return is_empty or soft_validate(index, value) or hard_validate(index, value)
|
|
393
|
+
|
|
394
|
+
return _filter_list_errors(map(validate, enumerate(node.get(list_key, []))))
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def valid_list_sum(blank_nodes: list):
|
|
398
|
+
values = [_value_average(p, default=None) for p in blank_nodes]
|
|
399
|
+
values_number = list(filter(is_number, values))
|
|
400
|
+
is_valid = len(values) == len(values_number)
|
|
401
|
+
return list_sum(values_number), is_valid
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def validate_list_sum_100_percent(node: dict, list_key: str):
|
|
405
|
+
def validate(values: list):
|
|
406
|
+
term_ids = [v["node"].get("term", {}).get("@id") for v in values]
|
|
407
|
+
total_value, valid_sum = valid_list_sum([v["node"] for v in values])
|
|
408
|
+
blank_node = values[0]
|
|
409
|
+
min_value = 99.5
|
|
410
|
+
max_value = 100.5
|
|
411
|
+
sum_equal_100 = blank_node.get("sumIs100Group")
|
|
412
|
+
valid = all(
|
|
413
|
+
[total_value <= max_value, not sum_equal_100 or total_value >= min_value]
|
|
414
|
+
)
|
|
415
|
+
return valid or [
|
|
416
|
+
{
|
|
417
|
+
"level": "error",
|
|
418
|
+
"dataPath": f".{list_key}[{value.get('index')}]",
|
|
419
|
+
"message": f"value should sum to {'' if sum_equal_100 else 'maximum '}100 across all values",
|
|
420
|
+
"params": {"termIds": term_ids, "sum": total_value, "max": max_value}
|
|
421
|
+
| ({"min": min_value} if sum_equal_100 else {}),
|
|
422
|
+
}
|
|
423
|
+
for value in values
|
|
424
|
+
]
|
|
425
|
+
|
|
426
|
+
groupped_values = group_blank_nodes(enumerate(node.get(list_key, []))).values()
|
|
427
|
+
return _filter_list_errors(flatten(map(validate, groupped_values)))
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def validate_list_percent_requires_value(
|
|
431
|
+
node: dict, list_key: str, term_types: List[TermTermType] = []
|
|
432
|
+
):
|
|
433
|
+
term_types_str = [t.value for t in term_types]
|
|
434
|
+
|
|
435
|
+
def validate(values: tuple):
|
|
436
|
+
index, blank_node = values
|
|
437
|
+
term = blank_node.get("term", {})
|
|
438
|
+
validate_value = all(
|
|
439
|
+
[
|
|
440
|
+
term.get("termType") in term_types_str,
|
|
441
|
+
term.get("units", "").startswith("%"),
|
|
442
|
+
]
|
|
443
|
+
)
|
|
444
|
+
value = blank_node.get("value", [])
|
|
445
|
+
return (
|
|
446
|
+
not validate_value
|
|
447
|
+
or len(value) > 0
|
|
448
|
+
or {
|
|
449
|
+
"level": "error",
|
|
450
|
+
"dataPath": f".{list_key}[{index}]",
|
|
451
|
+
"message": "should have required property 'value'",
|
|
452
|
+
"params": {"term": term, "missingProperty": "value"},
|
|
453
|
+
}
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
return _filter_list_errors(map(validate, enumerate(node.get(list_key, []))))
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def validate_list_valueType(node: dict, list_key: str):
|
|
460
|
+
def validate(values: tuple):
|
|
461
|
+
index, blank_node = values
|
|
462
|
+
term = blank_node.get("term", {})
|
|
463
|
+
expected_value_type = term_valueType(term)
|
|
464
|
+
value = blank_node.get("value")
|
|
465
|
+
return (
|
|
466
|
+
value is None
|
|
467
|
+
or match_value_type(expected_value_type, value)
|
|
468
|
+
or {
|
|
469
|
+
"level": "error",
|
|
470
|
+
"dataPath": f".{list_key}[{index}].value",
|
|
471
|
+
"message": "the node value type is incorrect",
|
|
472
|
+
"params": {"expected": expected_value_type},
|
|
473
|
+
}
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
return _filter_list_errors(map(validate, enumerate(node.get(list_key, []))))
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def validate_is_region(node: dict, region_key="region"):
|
|
480
|
+
region_id = node.get(region_key, {}).get("@id", "")
|
|
481
|
+
level = id_to_level(region_id)
|
|
482
|
+
return (
|
|
483
|
+
region_id == ""
|
|
484
|
+
or level > 0
|
|
485
|
+
or {
|
|
486
|
+
"level": "error",
|
|
487
|
+
"dataPath": f".{region_key}",
|
|
488
|
+
"message": "must not be a country",
|
|
489
|
+
}
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def validate_region_in_country(node: dict, region_key="region"):
|
|
494
|
+
country = node.get("country", {})
|
|
495
|
+
region_id = node.get(region_key, {}).get("@id", "")
|
|
496
|
+
return (
|
|
497
|
+
region_id == ""
|
|
498
|
+
or region_id[0:8] == country.get("@id")
|
|
499
|
+
or {
|
|
500
|
+
"level": "error",
|
|
501
|
+
"dataPath": f".{region_key}",
|
|
502
|
+
"message": "must be within the country",
|
|
503
|
+
"params": {"country": country.get("name")},
|
|
504
|
+
}
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def validate_country(node: dict):
|
|
509
|
+
country_id = node.get("country", {}).get("@id", "")
|
|
510
|
+
# handle additional regions used as country, like region-world
|
|
511
|
+
is_region = country_id.startswith("region-")
|
|
512
|
+
return (
|
|
513
|
+
country_id == ""
|
|
514
|
+
or is_region
|
|
515
|
+
or bool(re.search(r"GADM-[A-Z]{3}$", country_id))
|
|
516
|
+
or {"level": "error", "dataPath": ".country", "message": "must be a country"}
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def validate_country_region(node: dict):
|
|
521
|
+
return _filter_list_errors(
|
|
522
|
+
[
|
|
523
|
+
validate_country(node),
|
|
524
|
+
validate_is_region(node),
|
|
525
|
+
validate_region_in_country(node),
|
|
526
|
+
]
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def validate_list_country_region(node: dict, list_key: str):
|
|
531
|
+
def validate(values: tuple):
|
|
532
|
+
index, blank_node = values
|
|
533
|
+
errors = [
|
|
534
|
+
validate_country(blank_node),
|
|
535
|
+
validate_is_region(blank_node),
|
|
536
|
+
validate_region_in_country(blank_node),
|
|
537
|
+
]
|
|
538
|
+
errors = [
|
|
539
|
+
update_error_path(error, list_key, index)
|
|
540
|
+
for error in errors
|
|
541
|
+
if error is not True
|
|
542
|
+
]
|
|
543
|
+
return _filter_list_errors(errors)
|
|
544
|
+
|
|
545
|
+
return _filter_list_errors(map(validate, enumerate(node.get(list_key, []))))
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def need_validate_coordinates(node: dict):
|
|
549
|
+
return gee_is_enabled() and "latitude" in node and "longitude" in node
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def validate_coordinates(node: dict):
|
|
553
|
+
latitude = node.get("latitude")
|
|
554
|
+
longitude = node.get("longitude")
|
|
555
|
+
country = node.get("country", {})
|
|
556
|
+
region = node.get("region")
|
|
557
|
+
gadm_id = region.get("@id") if region else country.get("@id")
|
|
558
|
+
id = get_region_id(node)
|
|
559
|
+
return gadm_id == id or {
|
|
560
|
+
"level": "error",
|
|
561
|
+
"dataPath": ".region" if region else ".country",
|
|
562
|
+
"message": "does not contain latitude and longitude",
|
|
563
|
+
"params": {
|
|
564
|
+
"current": gadm_id,
|
|
565
|
+
"expected": id,
|
|
566
|
+
"distance": get_region_distance(
|
|
567
|
+
gadm_id, latitude=latitude, longitude=longitude
|
|
568
|
+
),
|
|
569
|
+
},
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def need_validate_area(node: dict):
|
|
574
|
+
return all(["area" in node, "boundary" in node, "boundaryArea" in node])
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def validate_area(node: dict):
|
|
578
|
+
threshold = 0.05
|
|
579
|
+
value = round(node.get("area", 0), 1)
|
|
580
|
+
expected_value = round(node.get("boundaryArea", 0), 1)
|
|
581
|
+
delta = value_difference(value, expected_value)
|
|
582
|
+
return delta < threshold or {
|
|
583
|
+
"level": "warning",
|
|
584
|
+
"dataPath": ".area",
|
|
585
|
+
"message": "should be equal to boundary",
|
|
586
|
+
"params": {
|
|
587
|
+
"current": value,
|
|
588
|
+
"expected": expected_value,
|
|
589
|
+
"delta": delta * 100,
|
|
590
|
+
"threshold": threshold,
|
|
591
|
+
},
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def validate_boundary_area(node: dict):
|
|
596
|
+
area = node.get("boundaryArea", 0) / 100
|
|
597
|
+
return area < MAX_AREA_SIZE or {
|
|
598
|
+
"level": "warning",
|
|
599
|
+
"dataPath": ".boundaryArea",
|
|
600
|
+
"message": "should be lower than max size",
|
|
601
|
+
"params": {"current": area, "expected": MAX_AREA_SIZE},
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def need_validate_region_size(node: dict):
|
|
606
|
+
return all(
|
|
607
|
+
[
|
|
608
|
+
gee_is_enabled(),
|
|
609
|
+
not need_validate_coordinates(node),
|
|
610
|
+
"boundaryArea" not in node,
|
|
611
|
+
"region" in node or "country" in node,
|
|
612
|
+
]
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def validate_region_size(node: dict):
|
|
617
|
+
region_id = node.get("region", node.get("country", {})).get("@id")
|
|
618
|
+
region = download_hestia(region_id) if region_id else {}
|
|
619
|
+
try:
|
|
620
|
+
from hestia_earth.earth_engine.gadm import get_size_km2
|
|
621
|
+
|
|
622
|
+
# get_region_size might throw error is geometry has too many edges
|
|
623
|
+
area = region.get("area", get_size_km2(region_id) if region_id else None) or 0
|
|
624
|
+
except Exception:
|
|
625
|
+
area = 0
|
|
626
|
+
return area < MAX_AREA_SIZE or {
|
|
627
|
+
"level": "warning",
|
|
628
|
+
"dataPath": f".{'region' if node.get('region') else 'country'}",
|
|
629
|
+
"message": "should be lower than max size",
|
|
630
|
+
"params": {"current": area, "expected": MAX_AREA_SIZE},
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
N_A_VALUES = ["#n/a", "#na", "n/a", "na", "n.a", "nodata", "no data"]
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def validate_empty_fields(node: dict):
|
|
638
|
+
keys = list(filter(lambda key: isinstance(node.get(key), str), node.keys()))
|
|
639
|
+
|
|
640
|
+
def validate(key: str):
|
|
641
|
+
return not node.get(key).lower() in N_A_VALUES or {
|
|
642
|
+
"level": "warning",
|
|
643
|
+
"dataPath": f".{key}",
|
|
644
|
+
"message": "may not be empty",
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return _filter_list_errors(map(validate, keys), False)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def validate_linked_source_privacy(node: dict, key: str, node_map: dict = {}):
|
|
651
|
+
related_source = find_linked_node(node_map, node.get(key, {}))
|
|
652
|
+
node_privacy = node.get("dataPrivate")
|
|
653
|
+
related_source_privacy = (
|
|
654
|
+
related_source.get("dataPrivate") if related_source else None
|
|
655
|
+
)
|
|
656
|
+
return (
|
|
657
|
+
related_source_privacy is None
|
|
658
|
+
or node_privacy == related_source_privacy
|
|
659
|
+
or {
|
|
660
|
+
"level": "error",
|
|
661
|
+
"dataPath": ".dataPrivate",
|
|
662
|
+
"message": "should have the same privacy as the related source",
|
|
663
|
+
"params": {
|
|
664
|
+
"dataPrivate": node_privacy,
|
|
665
|
+
key: {"dataPrivate": related_source_privacy},
|
|
666
|
+
},
|
|
667
|
+
}
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def validate_private_has_source(node: dict, key: str):
|
|
672
|
+
node_private = node.get("dataPrivate")
|
|
673
|
+
return any(
|
|
674
|
+
[not _VALIDATE_PRIVATE_SOURCE, not node_private, node.get(key) is not None]
|
|
675
|
+
) or {
|
|
676
|
+
"level": "warning",
|
|
677
|
+
"dataPath": ".dataPrivate",
|
|
678
|
+
"message": "should add a source",
|
|
679
|
+
"params": {"current": key},
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def value_difference(value: float, expected_value: float):
|
|
684
|
+
return (
|
|
685
|
+
0
|
|
686
|
+
if any(
|
|
687
|
+
[
|
|
688
|
+
isinstance(expected_value, list),
|
|
689
|
+
expected_value == 0,
|
|
690
|
+
expected_value is None,
|
|
691
|
+
isinstance(value, list),
|
|
692
|
+
value is None,
|
|
693
|
+
]
|
|
694
|
+
)
|
|
695
|
+
else round(abs(value - expected_value) / expected_value, 4)
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def is_value_different(
|
|
700
|
+
value: float, expected_value: float, delta: float = 0.05
|
|
701
|
+
) -> bool:
|
|
702
|
+
return value_difference(value, expected_value) > delta
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _parse_node_value(node: dict):
|
|
706
|
+
def parse_list_value(value: list):
|
|
707
|
+
return list_sum(value) if len(value) > 0 else None
|
|
708
|
+
|
|
709
|
+
value = node.get("value")
|
|
710
|
+
return (
|
|
711
|
+
None
|
|
712
|
+
if value is None
|
|
713
|
+
else (parse_list_value(value) if isinstance(value, list) else value)
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def _get_term_recalculated_max_delta(term: dict):
|
|
718
|
+
col_name = "valueToleranceToHestiaRecalculatedValue"
|
|
719
|
+
return safe_parse_float(get_lookup_value(term, col_name), default=5) / 100
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _validate_list_model(node: dict, list_key: str):
|
|
723
|
+
def validate(values: tuple):
|
|
724
|
+
index, blank_node = values
|
|
725
|
+
term = blank_node.get("term", {})
|
|
726
|
+
max_delta = _get_term_recalculated_max_delta(term)
|
|
727
|
+
try:
|
|
728
|
+
method_tier = blank_node.get("methodTier")
|
|
729
|
+
value = _parse_node_value(blank_node)
|
|
730
|
+
# skip validation if `value` is not set
|
|
731
|
+
result = (
|
|
732
|
+
run_model_from_node(blank_node, node) if value is not None else None
|
|
733
|
+
)
|
|
734
|
+
expected_value = value_from_model(result) if result else 0
|
|
735
|
+
expected_method_tier = method_tier_from_model(result)
|
|
736
|
+
delta = value_difference(value, expected_value)
|
|
737
|
+
return (
|
|
738
|
+
method_tier != expected_method_tier
|
|
739
|
+
or delta < max_delta
|
|
740
|
+
or {
|
|
741
|
+
"level": "error",
|
|
742
|
+
"dataPath": f".{list_key}[{index}].value",
|
|
743
|
+
"message": "the value provided is not consistent with the model result",
|
|
744
|
+
"params": {
|
|
745
|
+
"model": blank_node.get("methodModel", {}),
|
|
746
|
+
"term": term,
|
|
747
|
+
"current": value,
|
|
748
|
+
"expected": expected_value,
|
|
749
|
+
"delta": to_precision(delta * 100, 4),
|
|
750
|
+
"threshold": max_delta,
|
|
751
|
+
},
|
|
752
|
+
}
|
|
753
|
+
)
|
|
754
|
+
except Exception:
|
|
755
|
+
return True
|
|
756
|
+
|
|
757
|
+
return validate
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def validate_list_model(node: dict, list_key: str) -> list:
|
|
761
|
+
nodes = node.get(list_key, []) if models_is_enabled() else []
|
|
762
|
+
with ThreadPoolExecutor() as executor:
|
|
763
|
+
errors = list(
|
|
764
|
+
executor.map(_validate_list_model(node, list_key), enumerate(nodes))
|
|
765
|
+
)
|
|
766
|
+
return _filter_list_errors(errors)
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def _reset_completeness(node: dict):
|
|
770
|
+
completeness = node.get("completeness", {})
|
|
771
|
+
completeness = reduce(
|
|
772
|
+
lambda prev, curr: {**prev, curr: False}, completeness.keys(), completeness
|
|
773
|
+
)
|
|
774
|
+
return {**node, "completeness": completeness}
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def _get_model_from_result(result: dict):
|
|
778
|
+
return result.get("methodModel", result.get("model")) if result else None
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def _validate_list_model_config(node: dict, list_key: str, conf: dict):
|
|
782
|
+
def validate_model(term: dict, value: float, index: int, model_conf: dict):
|
|
783
|
+
node_run = (
|
|
784
|
+
_reset_completeness(node)
|
|
785
|
+
if model_conf.get("resetDataCompleteness", False)
|
|
786
|
+
else node
|
|
787
|
+
)
|
|
788
|
+
expected_result = run_model(model_conf["model"], term.get("@id"), node_run)
|
|
789
|
+
expected_value = value_from_model(expected_result)
|
|
790
|
+
delta = value_difference(value, expected_value)
|
|
791
|
+
return delta < model_conf["delta"] or {
|
|
792
|
+
"level": model_conf.get("level", "error"),
|
|
793
|
+
"dataPath": f".{list_key}[{index}].value",
|
|
794
|
+
"message": "the value provided is not consistent with the model result",
|
|
795
|
+
"params": {
|
|
796
|
+
"model": _get_model_from_result(expected_result[0]),
|
|
797
|
+
"term": term,
|
|
798
|
+
"current": value,
|
|
799
|
+
"expected": expected_value,
|
|
800
|
+
"delta": to_precision(delta * 100, 4),
|
|
801
|
+
"threshold": model_conf["delta"],
|
|
802
|
+
},
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
def validate(values: tuple):
|
|
806
|
+
index, blank_node = values
|
|
807
|
+
value = _parse_node_value(blank_node)
|
|
808
|
+
term = blank_node.get("term", {})
|
|
809
|
+
term_id = blank_node.get("term", {}).get("@id")
|
|
810
|
+
# get the configuration for this element
|
|
811
|
+
# if it does not exist or no `value` is set, skip model
|
|
812
|
+
term_conf = conf.get(term_id)
|
|
813
|
+
return (
|
|
814
|
+
validate_model(term, value, index, term_conf)
|
|
815
|
+
if term_conf and value is not None
|
|
816
|
+
else True
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
return validate
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def validate_list_model_config(node: dict, list_key: str, conf: dict):
|
|
823
|
+
"""
|
|
824
|
+
Validates a list using the engine models.
|
|
825
|
+
This method uses a configuration to determine which `term` in the elements should run.
|
|
826
|
+
It does not use the `methodModel` that could be found on each element.
|
|
827
|
+
|
|
828
|
+
Parameters
|
|
829
|
+
----------
|
|
830
|
+
node : dict
|
|
831
|
+
The node containing the list to run.
|
|
832
|
+
list_key : str
|
|
833
|
+
The property of the node containing the list to run.
|
|
834
|
+
conf : dict
|
|
835
|
+
The configuration to decide which models to run.
|
|
836
|
+
|
|
837
|
+
Returns
|
|
838
|
+
-------
|
|
839
|
+
list
|
|
840
|
+
List of errors from the models or `True` if no errors.
|
|
841
|
+
"""
|
|
842
|
+
nodes = node.get(list_key, []) if models_is_enabled() else []
|
|
843
|
+
with ThreadPoolExecutor() as executor:
|
|
844
|
+
errors = list(
|
|
845
|
+
executor.map(
|
|
846
|
+
_validate_list_model_config(node, list_key, conf), enumerate(nodes)
|
|
847
|
+
)
|
|
848
|
+
)
|
|
849
|
+
return _filter_list_errors(errors)
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def _unique_term_grouping(term_id: str):
|
|
853
|
+
# TODO: use a lookup instead
|
|
854
|
+
return (
|
|
855
|
+
re.split(
|
|
856
|
+
r"(Kg|Liveweight|ColdCarcassWeight|ColdDressedCarcassWeight|ReadyToCookWeight)",
|
|
857
|
+
term_id,
|
|
858
|
+
)[0]
|
|
859
|
+
if term_id
|
|
860
|
+
else None
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def validate_duplicated_term_units(
|
|
865
|
+
node: dict, list_key: str, term_types: List[TermTermType]
|
|
866
|
+
):
|
|
867
|
+
def term_ids_mapper(prev: dict, curr: dict):
|
|
868
|
+
term = curr.get("term", {})
|
|
869
|
+
term_id = term.get("@id")
|
|
870
|
+
term_id_suffix = _unique_term_grouping(term_id)
|
|
871
|
+
prev[term_id_suffix] = prev.get(term_id_suffix, []) + [term.get("units")]
|
|
872
|
+
return prev
|
|
873
|
+
|
|
874
|
+
blank_nodes = node.get(list_key, [])
|
|
875
|
+
term_ids_to_units = reduce(
|
|
876
|
+
term_ids_mapper, filter_list_term_type(blank_nodes, term_types), {}
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
def validate(values: tuple):
|
|
880
|
+
index, blank_node = values
|
|
881
|
+
term = blank_node.get("term", {})
|
|
882
|
+
term_id = term.get("@id")
|
|
883
|
+
term_id_suffix = _unique_term_grouping(term_id)
|
|
884
|
+
units = term_ids_to_units.get(term_id_suffix, [])
|
|
885
|
+
return len(units) <= 1 or {
|
|
886
|
+
"level": "warning",
|
|
887
|
+
"dataPath": f".{list_key}[{index}].term",
|
|
888
|
+
"message": "should not use identical terms with different units",
|
|
889
|
+
"params": {"term": term, "units": units},
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
return _filter_list_errors(map(validate, enumerate(blank_nodes)))
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def validate_other_model(node: dict, list_key: str):
|
|
896
|
+
def validate(values: tuple):
|
|
897
|
+
index, blank_node = values
|
|
898
|
+
term = blank_node.get("methodModel", {})
|
|
899
|
+
term_id = term.get("@id")
|
|
900
|
+
return (
|
|
901
|
+
term_id != OTHER_MODEL_ID
|
|
902
|
+
or bool(blank_node.get("methodModelDescription"))
|
|
903
|
+
or {
|
|
904
|
+
"level": "error",
|
|
905
|
+
"dataPath": f".{list_key}[{index}].methodModel",
|
|
906
|
+
"message": "is required when using other model",
|
|
907
|
+
}
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
return _filter_list_errors(map(validate, enumerate(node.get(list_key, []))))
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
def validate_nested_existing_node(node: dict, key: str):
|
|
914
|
+
# detect when a non-indexed node references indexed nodes
|
|
915
|
+
is_indexed = "@id" in node
|
|
916
|
+
nested_value = node.get(key)
|
|
917
|
+
return is_indexed or (
|
|
918
|
+
_filter_list_errors(
|
|
919
|
+
[
|
|
920
|
+
"@id" not in value
|
|
921
|
+
or {
|
|
922
|
+
"level": "warning",
|
|
923
|
+
"dataPath": f".{key}[{index}].@id",
|
|
924
|
+
"message": "should not link to an existing node",
|
|
925
|
+
"params": {"node": value},
|
|
926
|
+
}
|
|
927
|
+
for index, value in enumerate(nested_value)
|
|
928
|
+
]
|
|
929
|
+
)
|
|
930
|
+
if isinstance(nested_value, list)
|
|
931
|
+
else (
|
|
932
|
+
"@id" not in (nested_value or {})
|
|
933
|
+
or {
|
|
934
|
+
"level": "warning",
|
|
935
|
+
"dataPath": f".{key}.@id",
|
|
936
|
+
"message": "should not link to an existing node",
|
|
937
|
+
"params": {"node": nested_value},
|
|
938
|
+
}
|
|
939
|
+
)
|
|
940
|
+
)
|