@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,88 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from hestia_earth.schema import SchemaType, TermTermType
|
|
5
|
+
from hestia_earth.utils.api import search
|
|
6
|
+
|
|
7
|
+
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
8
|
+
RESULTS_PATH = os.path.join(CURRENT_DIR, "search-results.json")
|
|
9
|
+
|
|
10
|
+
_CACHE = {}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _load_results(filepath: str) -> dict:
|
|
14
|
+
with open(filepath) as f:
|
|
15
|
+
return json.load(f)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TERMS_QUERY(Enum):
|
|
19
|
+
FUEL = TermTermType.FUEL.value
|
|
20
|
+
CROP_RESIDUE = TermTermType.CROPRESIDUE.value
|
|
21
|
+
MODEL = TermTermType.MODEL.value
|
|
22
|
+
FORAGE = TermTermType.FORAGE.value
|
|
23
|
+
RICE = "rice"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_terms_query = {
|
|
27
|
+
TERMS_QUERY.FUEL: {
|
|
28
|
+
"should": [
|
|
29
|
+
{
|
|
30
|
+
"bool": {
|
|
31
|
+
"must": [{"match": {"termType": TermTermType.FUEL.value}}],
|
|
32
|
+
"should": [
|
|
33
|
+
{"match": {"name": "gasoline"}},
|
|
34
|
+
{"match": {"name": "petrol"}},
|
|
35
|
+
{"match": {"name": "diesel"}},
|
|
36
|
+
],
|
|
37
|
+
"minimum_should_match": 1,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"minimum_should_match": 1,
|
|
42
|
+
},
|
|
43
|
+
TERMS_QUERY.CROP_RESIDUE: {
|
|
44
|
+
"should": [{"match": {"termType": TermTermType.CROPRESIDUE.value}}],
|
|
45
|
+
"minimum_should_match": 1,
|
|
46
|
+
},
|
|
47
|
+
TERMS_QUERY.MODEL: {
|
|
48
|
+
"should": [{"match": {"termType": TermTermType.MODEL.value}}],
|
|
49
|
+
"minimum_should_match": 1,
|
|
50
|
+
},
|
|
51
|
+
TERMS_QUERY.FORAGE: {
|
|
52
|
+
"should": [
|
|
53
|
+
{"match": {"termType.keyword": TermTermType.CROP.value}},
|
|
54
|
+
{"match": {"termType.keyword": TermTermType.FORAGE.value}},
|
|
55
|
+
{"match": {"name": "forage"}},
|
|
56
|
+
],
|
|
57
|
+
"minimum_should_match": 2,
|
|
58
|
+
},
|
|
59
|
+
TERMS_QUERY.RICE: {
|
|
60
|
+
"should": [
|
|
61
|
+
{"match": {"termType.keyword": TermTermType.CROP.value}},
|
|
62
|
+
{"match": {"name": "rice"}},
|
|
63
|
+
],
|
|
64
|
+
"minimum_should_match": 2,
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _exec_query(query: dict) -> list[str]:
|
|
70
|
+
terms = search(
|
|
71
|
+
{"bool": {"must": [{"match": {"@type": SchemaType.TERM.value}}]} | query},
|
|
72
|
+
limit=10000,
|
|
73
|
+
)
|
|
74
|
+
return list(map(lambda n: n["@id"], terms))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_terms(query: TERMS_QUERY):
|
|
78
|
+
return _CACHE.get(query.value) or _exec_query(_terms_query[query])
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_all_terms():
|
|
82
|
+
return {key.value: _exec_query(value) for key, value in _terms_query.items()}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def enable_mock(filepath: str):
|
|
86
|
+
global _CACHE # noqa: F824
|
|
87
|
+
_CACHE = _load_results(filepath)
|
|
88
|
+
return _CACHE
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import List
|
|
3
|
+
from functools import reduce, lru_cache
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
from hestia_earth.schema import NodeType, TermTermType, UNIQUENESS_FIELDS
|
|
6
|
+
from hestia_earth.utils.api import download_hestia
|
|
7
|
+
from hestia_earth.utils.lookup import download_lookup, get_table_value
|
|
8
|
+
from hestia_earth.utils.tools import (
|
|
9
|
+
list_average,
|
|
10
|
+
safe_parse_date,
|
|
11
|
+
safe_parse_float,
|
|
12
|
+
non_empty_list,
|
|
13
|
+
is_number,
|
|
14
|
+
is_boolean,
|
|
15
|
+
get_dict_key,
|
|
16
|
+
flatten,
|
|
17
|
+
)
|
|
18
|
+
from hestia_earth.utils.model import filter_list_term_type, find_primary_product
|
|
19
|
+
|
|
20
|
+
ANIMAL_TERM_TYPES = [TermTermType.LIVEANIMAL, TermTermType.LIVEAQUATICSPECIES]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@lru_cache()
|
|
24
|
+
def _get_term_lookup_value(term_id: str, term_type: str, column: str):
|
|
25
|
+
lookup = download_lookup(f"{term_type}.csv", keep_in_memory=True)
|
|
26
|
+
return get_table_value(lookup, "term.id", term_id, column)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_lookup_value(lookup_term: dict, column: str):
|
|
30
|
+
value = (
|
|
31
|
+
_get_term_lookup_value(
|
|
32
|
+
lookup_term.get("@id"), lookup_term.get("termType"), column
|
|
33
|
+
)
|
|
34
|
+
if lookup_term
|
|
35
|
+
else None
|
|
36
|
+
)
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _next_error(values: list):
|
|
41
|
+
return next((x for x in values if x is not True), True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _filter_list_errors(values: list, return_single=True):
|
|
45
|
+
values = list(filter(lambda x: x is not True, values))
|
|
46
|
+
return (
|
|
47
|
+
True
|
|
48
|
+
if return_single and len(values) == 0
|
|
49
|
+
else (values[0] if return_single and len(values) == 1 else values)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _flatten_errors(errors):
|
|
54
|
+
errors_list = [
|
|
55
|
+
[] if isinstance(v, bool) else v if isinstance(v, list) else [v] for v in errors
|
|
56
|
+
]
|
|
57
|
+
return flatten(errors_list)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _list_except_item(values: list, item):
|
|
61
|
+
try:
|
|
62
|
+
idx = values.index(item)
|
|
63
|
+
return values[:idx] + values[idx + 1 :]
|
|
64
|
+
except ValueError:
|
|
65
|
+
return values
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def update_error_path(error: dict, key: str, index=None):
|
|
69
|
+
path = (
|
|
70
|
+
f".{key}[{index}]{error.get('dataPath')}"
|
|
71
|
+
if index is not None
|
|
72
|
+
else f".{key}{error.get('dataPath')}"
|
|
73
|
+
)
|
|
74
|
+
return error | {"dataPath": path}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _safe_cast(val, to_type, default=None):
|
|
78
|
+
try:
|
|
79
|
+
return to_type(val)
|
|
80
|
+
except (ValueError, TypeError):
|
|
81
|
+
return default
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def hash_dict(value: dict):
|
|
85
|
+
return json.dumps(value, sort_keys=True)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def is_same_dict(a: dict, b: dict):
|
|
89
|
+
return hash_dict(a) == hash_dict(b)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _dict_without_key(a: dict, key: str):
|
|
93
|
+
no_key = a.copy()
|
|
94
|
+
if key in no_key:
|
|
95
|
+
no_key.pop(key)
|
|
96
|
+
return no_key
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
_GROUP_NODE_TYPES = [
|
|
100
|
+
NodeType.CYCLE.value,
|
|
101
|
+
NodeType.IMPACTASSESSMENT.value,
|
|
102
|
+
NodeType.SITE.value,
|
|
103
|
+
NodeType.SOURCE.value,
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _is_node(value):
|
|
108
|
+
return (
|
|
109
|
+
isinstance(value, dict)
|
|
110
|
+
and (value.get("type") or value.get("@type")) in _GROUP_NODE_TYPES
|
|
111
|
+
and (value.get("id") or value.get("@id"))
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _nodes_grouper(nodes: List[dict], grouping_func):
|
|
116
|
+
def group(groups: dict, node: dict):
|
|
117
|
+
if _is_node(node):
|
|
118
|
+
id = node.get("id") or node.get("@id")
|
|
119
|
+
type = node.get("type") or node.get("@type")
|
|
120
|
+
grouping_func(groups, node, type, id)
|
|
121
|
+
return groups
|
|
122
|
+
|
|
123
|
+
# check for nested nodes
|
|
124
|
+
nested_nodes = flatten(
|
|
125
|
+
[
|
|
126
|
+
v
|
|
127
|
+
for node in nodes
|
|
128
|
+
for v in node.values()
|
|
129
|
+
if _is_node(v) or (isinstance(v, list) and len(v) > 0 and _is_node(v[0]))
|
|
130
|
+
]
|
|
131
|
+
)
|
|
132
|
+
data = reduce(group, nested_nodes, {})
|
|
133
|
+
return reduce(group, nodes, data)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _group_nodes(nodes: List[dict]):
|
|
137
|
+
def group_by(groups: dict, node: dict, type: str, id: str):
|
|
138
|
+
groups[type] = groups.get(type, {})
|
|
139
|
+
groups[type][id] = node
|
|
140
|
+
|
|
141
|
+
return _nodes_grouper(nodes, group_by)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _hash_nodes(nodes: List[dict]):
|
|
145
|
+
def group_by(groups: dict, node: dict, type: str, id: str):
|
|
146
|
+
# store the hash of the node without the `id` for uniqueness check
|
|
147
|
+
key = hash_dict(_dict_without_key(node, "id"))
|
|
148
|
+
groups[key] = groups.get(key, []) + [node]
|
|
149
|
+
|
|
150
|
+
return _nodes_grouper(nodes, group_by)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _list_sum(values: list, prop: str):
|
|
154
|
+
return sum(map(lambda v: _safe_cast(v.get(prop, 0), float, 0.0), values))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def list_sum_terms(values: list, term_ids=[], default=None):
|
|
158
|
+
average_values = non_empty_list(
|
|
159
|
+
[
|
|
160
|
+
_value_average(node, default=default)
|
|
161
|
+
for node in values
|
|
162
|
+
if node.get("term", {}).get("@id") in term_ids
|
|
163
|
+
]
|
|
164
|
+
)
|
|
165
|
+
return sum(average_values) if average_values else None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _compare_values(x, y):
|
|
169
|
+
return (
|
|
170
|
+
next((True for item in x if item in y), False)
|
|
171
|
+
if isinstance(x, list) and isinstance(y, list)
|
|
172
|
+
else x == y
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _same_properties(value: dict, props: List[str]):
|
|
177
|
+
def identical(test: dict):
|
|
178
|
+
same_values = list(
|
|
179
|
+
filter(
|
|
180
|
+
lambda x: _compare_values(
|
|
181
|
+
get_dict_key(value, x), get_dict_key(test, x)
|
|
182
|
+
),
|
|
183
|
+
props,
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
return test if len(same_values) == len(props) else None
|
|
187
|
+
|
|
188
|
+
return identical
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _value_average(node: dict, default=0, key="value"):
|
|
192
|
+
try:
|
|
193
|
+
value = node.get(key)
|
|
194
|
+
return (
|
|
195
|
+
list_average(value, default)
|
|
196
|
+
if isinstance(value, list)
|
|
197
|
+
else (value or default)
|
|
198
|
+
)
|
|
199
|
+
except Exception:
|
|
200
|
+
return default
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def term_id_prefix(term_id: str):
|
|
204
|
+
return term_id.split("Kg")[0]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _download_linked_node(node: dict):
|
|
208
|
+
data = (
|
|
209
|
+
download_hestia(node.get("@id"), node.get("@type"))
|
|
210
|
+
if node.get("@id") and node.get("@type")
|
|
211
|
+
else None
|
|
212
|
+
)
|
|
213
|
+
return data if (data or {}).get("@id") == node.get("@id") else None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def find_linked_node(node_map: dict, node: dict):
|
|
217
|
+
"""
|
|
218
|
+
Find the Node by type and id in the list of nodes.
|
|
219
|
+
"""
|
|
220
|
+
return node_map.get(node.get("type"), {}).get(
|
|
221
|
+
node.get("id") or node.get("@id")
|
|
222
|
+
) or _download_linked_node(node)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _value_as_array(data: dict, key: str):
|
|
226
|
+
value = data.get(key)
|
|
227
|
+
return value if isinstance(value, list) else non_empty_list([value])
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def find_related_nodes(
|
|
231
|
+
node_map: dict, node: dict, related_key: str, related_type: NodeType
|
|
232
|
+
):
|
|
233
|
+
"""
|
|
234
|
+
Find all nodes related to the same node via a key.
|
|
235
|
+
Example: find all Cycles related to a Site via the key "site".
|
|
236
|
+
|
|
237
|
+
Parameters
|
|
238
|
+
----------
|
|
239
|
+
node_map : dict
|
|
240
|
+
The list of all nodes to do cross-validation, grouped by `type` and `id`.
|
|
241
|
+
node : dict
|
|
242
|
+
The node the other nodes should be related to.
|
|
243
|
+
related_key : str
|
|
244
|
+
How the other nodes are related to the `node`.
|
|
245
|
+
related_type : NodeType
|
|
246
|
+
The type of the related nodes.
|
|
247
|
+
|
|
248
|
+
Returns
|
|
249
|
+
-------
|
|
250
|
+
List[dict]
|
|
251
|
+
The list of nodes related to the `node`.
|
|
252
|
+
"""
|
|
253
|
+
node_id = node.get("@id", node.get("id"))
|
|
254
|
+
nodes = node_map.get(related_type.value, {}).values()
|
|
255
|
+
return list(
|
|
256
|
+
{
|
|
257
|
+
n.get("@id", n.get("id")): n
|
|
258
|
+
for n in nodes
|
|
259
|
+
for related_node in _value_as_array(n, related_key)
|
|
260
|
+
if (related_node.get("@id", related_node.get("id")) == node_id)
|
|
261
|
+
}.values()
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _is_before_today(date: str):
|
|
266
|
+
return safe_parse_date(date).date() <= datetime.now().date()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _node_year(node: dict):
|
|
270
|
+
date = node.get("endDate", node.get("startDate"))
|
|
271
|
+
date = safe_parse_date(date) if date else None
|
|
272
|
+
return date.year if date else None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def is_live_animal_cycle(cycle: dict):
|
|
276
|
+
blank_nodes = cycle.get("animals", []) + cycle.get("products", [])
|
|
277
|
+
animals = filter_list_term_type(blank_nodes, ANIMAL_TERM_TYPES)
|
|
278
|
+
return len(animals) > 0
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def contains_grazing_animals(cycle: dict):
|
|
282
|
+
blank_nodes = cycle.get("animals", []) + cycle.get("products", [])
|
|
283
|
+
animals = filter_list_term_type(blank_nodes, ANIMAL_TERM_TYPES)
|
|
284
|
+
return any(
|
|
285
|
+
[v for v in animals if get_lookup_value(v.get("term", {}), "isGrazingAnimal")]
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _match_list_el(source: list, dest: list, key: str):
|
|
290
|
+
src_values = non_empty_list([get_dict_key(x, key) for x in source])
|
|
291
|
+
dest_values = non_empty_list([get_dict_key(x, key) for x in dest])
|
|
292
|
+
return sorted(src_values) == sorted(dest_values)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _match_el(source: dict, dest: dict, fields: list):
|
|
296
|
+
def match(key: str):
|
|
297
|
+
keys = key.split(".")
|
|
298
|
+
is_list = len(keys) >= 2 and (
|
|
299
|
+
isinstance(get_dict_key(source, keys[0]), list)
|
|
300
|
+
or isinstance(get_dict_key(dest, keys[0]), list)
|
|
301
|
+
)
|
|
302
|
+
return (
|
|
303
|
+
_match_list_el(
|
|
304
|
+
get_dict_key(source, keys[0]) or [],
|
|
305
|
+
get_dict_key(dest, keys[0]) or [],
|
|
306
|
+
".".join(keys[1:]),
|
|
307
|
+
)
|
|
308
|
+
if is_list
|
|
309
|
+
else get_dict_key(source, key) == get_dict_key(dest, key)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
return all(map(match, fields))
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def find_by_unique_product(node: dict, product: dict, list_key: str = "products"):
|
|
316
|
+
"""
|
|
317
|
+
Fallback to finding a product with unique keys if a single product has the same `term.@id`.
|
|
318
|
+
"""
|
|
319
|
+
products = node.get(list_key, [])
|
|
320
|
+
products = [p for p in products if _match_el(p, product, ["term.@id"])]
|
|
321
|
+
return products[0] if len(products) == 1 else None
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def find_by_product(node: dict, product: dict, list_key: str = "products"):
|
|
325
|
+
keys = UNIQUENESS_FIELDS.get(node.get("type", node.get("@type")), {}).get(
|
|
326
|
+
list_key, ["term.@id"]
|
|
327
|
+
)
|
|
328
|
+
products = node.get(list_key, [])
|
|
329
|
+
return next((p for p in products if _match_el(p, product, keys)), None)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def is_same_product(p1: dict, p2: dict):
|
|
333
|
+
return find_by_product({"type": NodeType.CYCLE.value, "products": [p1]}, p2)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _formatDepth(depth: str):
|
|
337
|
+
# handle float values
|
|
338
|
+
return str(int(depth)) if is_number(depth) else ""
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def blank_node_properties_group(blank_node: dict):
|
|
342
|
+
def property_group(property: dict):
|
|
343
|
+
return get_lookup_value(property.get("term", {}), "blankNodesGroup")
|
|
344
|
+
|
|
345
|
+
properties = blank_node.get("properties", [])
|
|
346
|
+
return "-".join(non_empty_list(map(property_group, properties)))
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _blank_node_sum_groups(blank_node: dict, allow_sum_100: bool = True):
|
|
350
|
+
term = blank_node.get("term", {})
|
|
351
|
+
sum_below_100_group = get_lookup_value(term, "sumMax100Group")
|
|
352
|
+
sum_equal_100_group = get_lookup_value(term, "sumIs100Group")
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
"sumMax100Group": sum_below_100_group
|
|
356
|
+
or (sum_equal_100_group if not allow_sum_100 else None),
|
|
357
|
+
"sumIs100Group": sum_equal_100_group if allow_sum_100 else None,
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def group_blank_nodes(nodes: list, by_sum: bool = True):
|
|
362
|
+
"""
|
|
363
|
+
Group a list of blank nodes using:
|
|
364
|
+
- the `depthUpper`, `depthLower`, `startDate`, `endDate`, `dates`
|
|
365
|
+
- the lookup group `sumMax100Group` or `sumIs100Group` if specified
|
|
366
|
+
- the lookup group `blankNodesGroup` on properties if any
|
|
367
|
+
|
|
368
|
+
Parameters
|
|
369
|
+
----------
|
|
370
|
+
nodes : list
|
|
371
|
+
List of blank nodes with their index.
|
|
372
|
+
by_sum : bool
|
|
373
|
+
Group blank nodes using the key to sum to 100% (`sumMax100Group` and `sumIs100Group`).
|
|
374
|
+
"""
|
|
375
|
+
|
|
376
|
+
def group_by(group: dict, values: tuple):
|
|
377
|
+
index, blank_node = values
|
|
378
|
+
properties_group = blank_node_properties_group(blank_node)
|
|
379
|
+
# note: grouping of `properties` disables the grouping == 100
|
|
380
|
+
sum_groups = _blank_node_sum_groups(
|
|
381
|
+
blank_node, allow_sum_100=not properties_group
|
|
382
|
+
)
|
|
383
|
+
keys = non_empty_list(
|
|
384
|
+
[
|
|
385
|
+
_formatDepth(blank_node.get("depthUpper", "")),
|
|
386
|
+
_formatDepth(blank_node.get("depthLower", "")),
|
|
387
|
+
blank_node.get("startDate"),
|
|
388
|
+
blank_node.get("endDate"),
|
|
389
|
+
"-".join(blank_node.get("dates") or []),
|
|
390
|
+
properties_group,
|
|
391
|
+
]
|
|
392
|
+
+ (list(sum_groups.values()) if by_sum else [])
|
|
393
|
+
)
|
|
394
|
+
key = "-".join(keys) if len(keys) > 0 else "default"
|
|
395
|
+
|
|
396
|
+
if not by_sum or all(
|
|
397
|
+
[blank_node.get("value", []), any(list(sum_groups.values()))]
|
|
398
|
+
):
|
|
399
|
+
group[key] = group.get(key, []) + [
|
|
400
|
+
{"index": index, "node": blank_node} | (sum_groups if by_sum else {})
|
|
401
|
+
]
|
|
402
|
+
|
|
403
|
+
return group
|
|
404
|
+
|
|
405
|
+
return reduce(group_by, nodes, {})
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def is_permanent_crop(cycle: dict):
|
|
409
|
+
product = find_primary_product(cycle) or {}
|
|
410
|
+
return (
|
|
411
|
+
get_lookup_value(product.get("term", {}), "cropGroupingFAO")
|
|
412
|
+
== "Permanent crops"
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def term_valueType(term: dict):
|
|
417
|
+
return get_lookup_value(term, "valueType")
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
VALUE_TYPE_MATCH = {"number": is_number, "boolean": is_boolean}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def match_value_type(value_type: str, value):
|
|
424
|
+
values = non_empty_list(value if isinstance(value, list) else [value])
|
|
425
|
+
return all([VALUE_TYPE_MATCH.get(value_type, lambda _: True)(v) for v in values])
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def cycle_start_date(cycle: dict) -> datetime:
|
|
429
|
+
product = find_primary_product(cycle)
|
|
430
|
+
max_cycleDuration = (
|
|
431
|
+
cycle.get("cycleDuration")
|
|
432
|
+
or safe_parse_float(
|
|
433
|
+
value=get_lookup_value(product["term"], "maximumCycleDuration"), default=0
|
|
434
|
+
)
|
|
435
|
+
if product
|
|
436
|
+
else 0
|
|
437
|
+
)
|
|
438
|
+
return (
|
|
439
|
+
safe_parse_date(cycle.get("startDate"))
|
|
440
|
+
if cycle.get("startDate")
|
|
441
|
+
else (
|
|
442
|
+
safe_parse_date(cycle.get("endDate")) - (timedelta(days=max_cycleDuration))
|
|
443
|
+
)
|
|
444
|
+
)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from hestia_earth.schema import SchemaType
|
|
3
|
+
from hestia_earth.utils.tools import flatten
|
|
4
|
+
|
|
5
|
+
from ..log import logger
|
|
6
|
+
from ..utils import update_error_path
|
|
7
|
+
from .shared import validate_empty_fields, validate_nodes_duplicates
|
|
8
|
+
from .cycle import validate_cycle
|
|
9
|
+
from .impact_assessment import validate_impact_assessment
|
|
10
|
+
from .organisation import validate_organisation
|
|
11
|
+
from .site import validate_site
|
|
12
|
+
from .source import validate_source
|
|
13
|
+
|
|
14
|
+
# disable validation based on `@type`
|
|
15
|
+
VALIDATE_EXISTING_NODES = os.getenv("VALIDATE_EXISTING_NODES", "false") == "true"
|
|
16
|
+
VALIDATE_TYPE = {
|
|
17
|
+
SchemaType.CYCLE.value: lambda n, nodes: validate_cycle(n, nodes),
|
|
18
|
+
SchemaType.IMPACTASSESSMENT.value: lambda n, nodes: validate_impact_assessment(
|
|
19
|
+
n, nodes
|
|
20
|
+
),
|
|
21
|
+
SchemaType.ORGANISATION.value: lambda n, nodes: validate_organisation(n, nodes),
|
|
22
|
+
SchemaType.SITE.value: lambda n, nodes: validate_site(n, nodes),
|
|
23
|
+
SchemaType.SOURCE.value: lambda n, nodes: validate_source(n, nodes),
|
|
24
|
+
}
|
|
25
|
+
SKIP_VALIDATE_DUPLICATES = [
|
|
26
|
+
SchemaType.ACTOR.value,
|
|
27
|
+
SchemaType.IMPACTASSESSMENT.value,
|
|
28
|
+
SchemaType.TERM.value,
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _has_keys(node: dict):
|
|
33
|
+
# ignore some keys that are automatically added to nested objects
|
|
34
|
+
ignore_keys = ["type", "id", "@type", "@id", "name", "description", "siteType"]
|
|
35
|
+
node_keys = [k for k in list(node.keys()) if k not in ignore_keys]
|
|
36
|
+
return len(node_keys) > 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _should_run(ntype: str, node: dict):
|
|
40
|
+
return all([ntype in VALIDATE_TYPE, _has_keys(node)])
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _validate_node_type(node_by_type: dict, node_by_hash: dict, ntype: str, node: dict):
|
|
44
|
+
should_run = _should_run(ntype, node)
|
|
45
|
+
if should_run:
|
|
46
|
+
logger.debug(
|
|
47
|
+
"Run validation on: type=%s, id=%s", ntype, node.get("id", node.get("@id"))
|
|
48
|
+
)
|
|
49
|
+
validator = VALIDATE_TYPE.get(ntype)
|
|
50
|
+
validations = validator(node, node_by_type) if should_run else []
|
|
51
|
+
return (
|
|
52
|
+
validations
|
|
53
|
+
+ validate_empty_fields(node)
|
|
54
|
+
+ (
|
|
55
|
+
[]
|
|
56
|
+
if any([ntype in SKIP_VALIDATE_DUPLICATES, not should_run])
|
|
57
|
+
else validate_nodes_duplicates(node, node_by_hash)
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _validate_node_children(node_by_type: dict, node_by_hash: dict, node: dict):
|
|
63
|
+
validations = []
|
|
64
|
+
for key, value in node.items():
|
|
65
|
+
if isinstance(value, list):
|
|
66
|
+
validations.extend(
|
|
67
|
+
[
|
|
68
|
+
_validate_node_child(node_by_type, node_by_hash, key, value, index)
|
|
69
|
+
for index, value in enumerate(value)
|
|
70
|
+
]
|
|
71
|
+
)
|
|
72
|
+
if isinstance(value, dict):
|
|
73
|
+
validations.append(
|
|
74
|
+
_validate_node_child(node_by_type, node_by_hash, key, value)
|
|
75
|
+
)
|
|
76
|
+
return flatten(validations)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _validate_node_child(
|
|
80
|
+
node_by_type: dict, node_by_hash: dict, key: str, value: dict, index=None
|
|
81
|
+
):
|
|
82
|
+
values = validate_node(node_by_type, node_by_hash)(value)
|
|
83
|
+
return list(
|
|
84
|
+
map(
|
|
85
|
+
lambda error: (
|
|
86
|
+
update_error_path(error, key, index)
|
|
87
|
+
if isinstance(error, dict)
|
|
88
|
+
else error
|
|
89
|
+
),
|
|
90
|
+
values,
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def validate_node(node_by_type: dict = {}, node_by_hash: dict = {}):
|
|
96
|
+
def validate(node: dict):
|
|
97
|
+
"""
|
|
98
|
+
Validates a single Node.
|
|
99
|
+
|
|
100
|
+
Parameters
|
|
101
|
+
----------
|
|
102
|
+
node : dict
|
|
103
|
+
The JSON-Node to validate.
|
|
104
|
+
|
|
105
|
+
Returns
|
|
106
|
+
-------
|
|
107
|
+
List
|
|
108
|
+
The list of errors/warnings for the node, which can be empty if no errors/warnings detected.
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
ntype = (
|
|
112
|
+
node.get("type", node.get("@type") if VALIDATE_EXISTING_NODES else None)
|
|
113
|
+
if isinstance(node, dict)
|
|
114
|
+
else None
|
|
115
|
+
)
|
|
116
|
+
return (
|
|
117
|
+
[]
|
|
118
|
+
if ntype is None
|
|
119
|
+
else list(
|
|
120
|
+
filter(
|
|
121
|
+
lambda v: v is not True,
|
|
122
|
+
flatten(
|
|
123
|
+
_validate_node_type(node_by_type, node_by_hash, ntype, node)
|
|
124
|
+
+ (
|
|
125
|
+
[]
|
|
126
|
+
if node.get("aggregated")
|
|
127
|
+
else _validate_node_children(
|
|
128
|
+
node_by_type, node_by_hash, node
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logger.error(
|
|
137
|
+
f"Error validating {ntype} with id '{node.get('id', node.get('@id'))}': {str(e)}"
|
|
138
|
+
)
|
|
139
|
+
raise e
|
|
140
|
+
|
|
141
|
+
return validate
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from hestia_earth.utils.model import find_primary_product
|
|
2
|
+
|
|
3
|
+
from hestia_earth.validation.utils import _filter_list_errors, get_lookup_value
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def validate_linked_impactAssessment(cycle: dict, list_key: str = "inputs"):
|
|
7
|
+
primary_product = find_primary_product(cycle) or {}
|
|
8
|
+
input_term_ids = (
|
|
9
|
+
get_lookup_value(primary_product.get("term"), "aggregationInputTermIds") or ""
|
|
10
|
+
).split(";")
|
|
11
|
+
|
|
12
|
+
def validate(values: tuple):
|
|
13
|
+
index, blank_node = values
|
|
14
|
+
is_aggregation_input = blank_node.get("term", {}).get("@id") in input_term_ids
|
|
15
|
+
linked_id = blank_node.get("impactAssessment", {}).get("@id", "")
|
|
16
|
+
is_valid = all([linked_id, "world" not in linked_id])
|
|
17
|
+
return (
|
|
18
|
+
not is_aggregation_input
|
|
19
|
+
or is_valid
|
|
20
|
+
or {
|
|
21
|
+
"level": "error",
|
|
22
|
+
"dataPath": f".{list_key}[{index}]{'.impactAssessment' if linked_id else ''}",
|
|
23
|
+
"message": "must be linked to a verified country-level Impact Assessment",
|
|
24
|
+
"params": {"expected": blank_node.get("country"), "current": linked_id},
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
_filter_list_errors(map(validate, enumerate(cycle.get(list_key, []))))
|
|
30
|
+
if input_term_ids
|
|
31
|
+
else True
|
|
32
|
+
)
|