@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.
Files changed (410) hide show
  1. package/.coveragerc +14 -0
  2. package/.dockerignore +19 -0
  3. package/.eslintignore +17 -0
  4. package/.eslintrc.js +11 -0
  5. package/.flake8 +5 -0
  6. package/.gitlab/issue_templates/new validation.md +82 -0
  7. package/.gitlab-ci.yml +216 -0
  8. package/.readthedocs.yml +24 -0
  9. package/CODEOWNERS +11 -0
  10. package/Dockerfile +13 -0
  11. package/LICENSE +21 -0
  12. package/MANIFEST.in +2 -0
  13. package/bin/hestia-validate-data +80 -0
  14. package/build_mocking.py +14 -0
  15. package/commitlint.config.js +1 -0
  16. package/docs/Makefile +20 -0
  17. package/docs/_static/styles.css +4 -0
  18. package/docs/_templates/custom-class-template.rst +34 -0
  19. package/docs/_templates/custom-module-template.rst +66 -0
  20. package/docs/_templates/layout.html +4 -0
  21. package/docs/conf.py +74 -0
  22. package/docs/index.rst +42 -0
  23. package/docs/make.bat +35 -0
  24. package/docs/requirements.txt +13 -0
  25. package/envs/.develop.env +1 -0
  26. package/envs/.master.env +1 -0
  27. package/guide-assets/.gitkeep +0 -0
  28. package/hestia_earth/validation/README.md +5 -0
  29. package/hestia_earth/validation/__init__.py +32 -0
  30. package/hestia_earth/validation/distribution.py +22 -0
  31. package/hestia_earth/validation/gee.py +162 -0
  32. package/hestia_earth/validation/log.py +44 -0
  33. package/hestia_earth/validation/models.py +141 -0
  34. package/hestia_earth/validation/preload_requests.py +61 -0
  35. package/hestia_earth/validation/terms.py +88 -0
  36. package/hestia_earth/validation/utils.py +444 -0
  37. package/hestia_earth/validation/validators/__init__.py +141 -0
  38. package/hestia_earth/validation/validators/aggregated_cycle.py +32 -0
  39. package/hestia_earth/validation/validators/aggregated_shared.py +37 -0
  40. package/hestia_earth/validation/validators/animal.py +88 -0
  41. package/hestia_earth/validation/validators/completeness.py +252 -0
  42. package/hestia_earth/validation/validators/cycle.py +1123 -0
  43. package/hestia_earth/validation/validators/distribution.py +86 -0
  44. package/hestia_earth/validation/validators/emission.py +109 -0
  45. package/hestia_earth/validation/validators/impact_assessment.py +138 -0
  46. package/hestia_earth/validation/validators/indicator.py +154 -0
  47. package/hestia_earth/validation/validators/infrastructure.py +25 -0
  48. package/hestia_earth/validation/validators/input.py +268 -0
  49. package/hestia_earth/validation/validators/management.py +131 -0
  50. package/hestia_earth/validation/validators/measurement.py +368 -0
  51. package/hestia_earth/validation/validators/organisation.py +43 -0
  52. package/hestia_earth/validation/validators/practice.py +590 -0
  53. package/hestia_earth/validation/validators/product.py +263 -0
  54. package/hestia_earth/validation/validators/property.py +266 -0
  55. package/hestia_earth/validation/validators/shared.py +940 -0
  56. package/hestia_earth/validation/validators/site.py +312 -0
  57. package/hestia_earth/validation/validators/source.py +20 -0
  58. package/hestia_earth/validation/validators/transformation.py +250 -0
  59. package/hestia_earth/validation/version.py +1 -0
  60. package/layer/build.sh +34 -0
  61. package/layer/deploy.sh +18 -0
  62. package/package.json +59 -0
  63. package/release.sh +11 -0
  64. package/requirements-ci.txt +6 -0
  65. package/requirements-test.txt +4 -0
  66. package/requirements.txt +2 -0
  67. package/run-docker-test.sh +7 -0
  68. package/run-docker.sh +9 -0
  69. package/run.py +99 -0
  70. package/scripts/build_docs.py +283 -0
  71. package/scripts/build_validation_list.py +160 -0
  72. package/scripts/guide-create-branch.sh +15 -0
  73. package/scripts/update-package-version.js +28 -0
  74. package/search-results.json +384 -0
  75. package/setup.cfg +2 -0
  76. package/setup.py +35 -0
  77. package/src/index.ts +1 -0
  78. package/src/validations.ts +22 -0
  79. package/src/version.ts +1 -0
  80. package/tests/Dockerfile +13 -0
  81. package/tests/__init__.py +3 -0
  82. package/tests/fixtures/aggregated/cycle/inputs-impactAssessment/invalid-no-impactAssessment.json +64 -0
  83. package/tests/fixtures/aggregated/cycle/inputs-impactAssessment/invalid-world.json +69 -0
  84. package/tests/fixtures/aggregated/cycle/inputs-impactAssessment/valid.json +69 -0
  85. package/tests/fixtures/animal/duplicated-input-cycle/invalid.json +98 -0
  86. package/tests/fixtures/animal/duplicated-input-cycle/valid.json +91 -0
  87. package/tests/fixtures/animal/pregnancyRateTotal/invalid.json +49 -0
  88. package/tests/fixtures/animal/pregnancyRateTotal/valid.json +60 -0
  89. package/tests/fixtures/animal/required/invalid.json +59 -0
  90. package/tests/fixtures/animal/required/valid.json +72 -0
  91. package/tests/fixtures/completeness/all-values/warning.json +22 -0
  92. package/tests/fixtures/completeness/animalPopulation/invalid.json +58 -0
  93. package/tests/fixtures/completeness/animalPopulation/valid-animals.json +71 -0
  94. package/tests/fixtures/completeness/animalPopulation/valid-incomplete.json +58 -0
  95. package/tests/fixtures/completeness/animalPopulation/valid-no-liveAnimals.json +37 -0
  96. package/tests/fixtures/completeness/blank-nodes/agri-food processor-invalid.json +52 -0
  97. package/tests/fixtures/completeness/blank-nodes/invalid.json +124 -0
  98. package/tests/fixtures/completeness/blank-nodes/valid.json +128 -0
  99. package/tests/fixtures/completeness/cropland/site.json +16 -0
  100. package/tests/fixtures/completeness/cropland/valid.json +22 -0
  101. package/tests/fixtures/completeness/cropland/warning.json +22 -0
  102. package/tests/fixtures/completeness/freshForage/error-animals.json +63 -0
  103. package/tests/fixtures/completeness/freshForage/error-products.json +65 -0
  104. package/tests/fixtures/completeness/freshForage/valid-animal-inputs.json +63 -0
  105. package/tests/fixtures/completeness/freshForage/valid-animals.json +63 -0
  106. package/tests/fixtures/completeness/freshForage/valid-not-grazing-liveAnimal.json +55 -0
  107. package/tests/fixtures/completeness/freshForage/valid-not-liveAnimal.json +47 -0
  108. package/tests/fixtures/completeness/freshForage/valid-products.json +68 -0
  109. package/tests/fixtures/completeness/ingredient/invalid-agri-food-processor.json +37 -0
  110. package/tests/fixtures/completeness/ingredient/invalid.json +49 -0
  111. package/tests/fixtures/completeness/ingredient/valid-agri-food-processor-complete.json +49 -0
  112. package/tests/fixtures/completeness/ingredient/valid-agri-food-processor-incomplete.json +37 -0
  113. package/tests/fixtures/completeness/ingredient/valid.json +49 -0
  114. package/tests/fixtures/completeness/material/error.json +49 -0
  115. package/tests/fixtures/completeness/material/valid-fuel-material.json +60 -0
  116. package/tests/fixtures/completeness/material/valid-incomplete.json +36 -0
  117. package/tests/fixtures/completeness/material/valid-no-fuel.json +36 -0
  118. package/tests/fixtures/completeness/valid.json +22 -0
  119. package/tests/fixtures/cycle/aboveGroundCropResidue/invalid.json +76 -0
  120. package/tests/fixtures/cycle/aboveGroundCropResidue/valid.json +76 -0
  121. package/tests/fixtures/cycle/aggregated-valid.json +102 -0
  122. package/tests/fixtures/cycle/coverCrop/invalid.json +64 -0
  123. package/tests/fixtures/cycle/coverCrop/valid-not-coverCrop.json +54 -0
  124. package/tests/fixtures/cycle/coverCrop/valid.json +64 -0
  125. package/tests/fixtures/cycle/cropResidue/complete/invalid.json +56 -0
  126. package/tests/fixtures/cycle/cropResidue/complete/valid.json +82 -0
  127. package/tests/fixtures/cycle/cropResidue/incomplete/invalid.json +42 -0
  128. package/tests/fixtures/cycle/cropResidue/incomplete/valid.json +56 -0
  129. package/tests/fixtures/cycle/dates/invalid-emissions.json +70 -0
  130. package/tests/fixtures/cycle/liveAnimal-animalProduct-mapping/invalid.json +63 -0
  131. package/tests/fixtures/cycle/liveAnimal-animalProduct-mapping/valid.json +63 -0
  132. package/tests/fixtures/cycle/maximumCycleDuration/invalid-dates-year-only.json +48 -0
  133. package/tests/fixtures/cycle/maximumCycleDuration/invalid-dates.json +48 -0
  134. package/tests/fixtures/cycle/maximumCycleDuration/invalid.json +48 -0
  135. package/tests/fixtures/cycle/maximumCycleDuration/valid-dates-year-only.json +48 -0
  136. package/tests/fixtures/cycle/maximumCycleDuration/valid-dates.json +48 -0
  137. package/tests/fixtures/cycle/maximumCycleDuration/valid.json +48 -0
  138. package/tests/fixtures/cycle/otherSites/cycleDuration/invalid.json +52 -0
  139. package/tests/fixtures/cycle/otherSites/cycleDuration/valid-no-siteDuration.json +40 -0
  140. package/tests/fixtures/cycle/otherSites/cycleDuration/valid.json +52 -0
  141. package/tests/fixtures/cycle/practices/stockingDensityPermanentPastureAverage/invalid.json +56 -0
  142. package/tests/fixtures/cycle/practices/stockingDensityPermanentPastureAverage/valid.json +65 -0
  143. package/tests/fixtures/cycle/primary-product-as-input/invalid.json +59 -0
  144. package/tests/fixtures/cycle/primary-product-as-input/valid.json +48 -0
  145. package/tests/fixtures/cycle/product-linked-ia/cycle.json +66 -0
  146. package/tests/fixtures/cycle/product-linked-ia/invalid-multiple.json +58 -0
  147. package/tests/fixtures/cycle/product-linked-ia/valid.json +57 -0
  148. package/tests/fixtures/cycle/products/animals/invalid.json +69 -0
  149. package/tests/fixtures/cycle/products/animals/valid.json +58 -0
  150. package/tests/fixtures/cycle/riceGrainInHuskFlooded-minimumCycleDuration/invalid.json +53 -0
  151. package/tests/fixtures/cycle/riceGrainInHuskFlooded-minimumCycleDuration/valid.json +53 -0
  152. package/tests/fixtures/cycle/siteDuration/crop/invalid.json +53 -0
  153. package/tests/fixtures/cycle/siteDuration/crop/valid-different-duration.json +53 -0
  154. package/tests/fixtures/cycle/siteDuration/crop/valid-same-duration.json +53 -0
  155. package/tests/fixtures/cycle/siteDuration/invalid.json +41 -0
  156. package/tests/fixtures/cycle/siteDuration/valid-no-siteDuration.json +40 -0
  157. package/tests/fixtures/cycle/siteDuration/valid-otherSites.json +48 -0
  158. package/tests/fixtures/cycle/siteDuration/valid.json +45 -0
  159. package/tests/fixtures/cycle/substrate/required/invalid.json +50 -0
  160. package/tests/fixtures/cycle/substrate/required/valid.json +60 -0
  161. package/tests/fixtures/cycle/valid.json +343 -0
  162. package/tests/fixtures/emission/linked-terms/inputs/invalid.json +78 -0
  163. package/tests/fixtures/emission/linked-terms/inputs/valid.json +106 -0
  164. package/tests/fixtures/emission/linked-terms/transformation/error.json +104 -0
  165. package/tests/fixtures/emission/linked-terms/transformation/valid.json +107 -0
  166. package/tests/fixtures/emission/linked-terms/transformation/warning.json +76 -0
  167. package/tests/fixtures/emission/methodTier-background/invalid.json +60 -0
  168. package/tests/fixtures/emission/methodTier-background/valid.json +60 -0
  169. package/tests/fixtures/emission/not-relevant/invalid.json +71 -0
  170. package/tests/fixtures/emission/not-relevant/valid.json +95 -0
  171. package/tests/fixtures/emission/not-relevant-methodTier/invalid.json +70 -0
  172. package/tests/fixtures/emission/not-relevant-methodTier/valid.json +95 -0
  173. package/tests/fixtures/impactAssessment/aggregated-valid.json +43 -0
  174. package/tests/fixtures/impactAssessment/cycle-contains-product/invalid.json +34 -0
  175. package/tests/fixtures/impactAssessment/cycle-contains-product/valid.json +34 -0
  176. package/tests/fixtures/impactAssessment/cycle-endDate/invalid.json +26 -0
  177. package/tests/fixtures/impactAssessment/cycle-endDate/valid.json +26 -0
  178. package/tests/fixtures/impactAssessment/valid.json +93 -0
  179. package/tests/fixtures/indicator/characterisedIndicator-methodModel/invalid.json +52 -0
  180. package/tests/fixtures/indicator/characterisedIndicator-methodModel/valid.json +52 -0
  181. package/tests/fixtures/indicator/ionisingCompounds/invalid.json +23 -0
  182. package/tests/fixtures/indicator/ionisingCompounds/valid.json +23 -0
  183. package/tests/fixtures/indicator/landTransformation/invalid-grouped.json +257 -0
  184. package/tests/fixtures/indicator/landTransformation/invalid.json +100 -0
  185. package/tests/fixtures/indicator/landTransformation/valid-grouped-full.json +507 -0
  186. package/tests/fixtures/indicator/landTransformation/valid-grouped.json +507 -0
  187. package/tests/fixtures/indicator/landTransformation/valid.json +100 -0
  188. package/tests/fixtures/infrastructure/lifespan/invalid.json +26 -0
  189. package/tests/fixtures/infrastructure/lifespan/valid.json +45 -0
  190. package/tests/fixtures/input/animalFeed-fate/invalid.json +103 -0
  191. package/tests/fixtures/input/animalFeed-fate/valid.json +90 -0
  192. package/tests/fixtures/input/country/invalid.json +64 -0
  193. package/tests/fixtures/input/country/valid.json +64 -0
  194. package/tests/fixtures/input/distribution/animalHousing.json +103 -0
  195. package/tests/fixtures/input/distribution/complete/invalid.json +177 -0
  196. package/tests/fixtures/input/distribution/complete/valid.json +163 -0
  197. package/tests/fixtures/input/distribution/incomplete/valid.json +139 -0
  198. package/tests/fixtures/input/impactAssessment/invalid.json +99 -0
  199. package/tests/fixtures/input/impactAssessment/valid.json +89 -0
  200. package/tests/fixtures/input/input-as-product/invalid.json +57 -0
  201. package/tests/fixtures/input/input-as-product/valid.json +59 -0
  202. package/tests/fixtures/input/mustIncludeId/invalid.json +13 -0
  203. package/tests/fixtures/input/mustIncludeId/valid-multiple-ids.json +31 -0
  204. package/tests/fixtures/input/mustIncludeId/valid.json +22 -0
  205. package/tests/fixtures/input/saplings/invalid.json +58 -0
  206. package/tests/fixtures/input/saplings/valid-no-saplings.json +58 -0
  207. package/tests/fixtures/input/saplings/valid-not-plantation.json +58 -0
  208. package/tests/fixtures/input/saplings/valid.json +58 -0
  209. package/tests/fixtures/integration/distribution/product-yield-invalid.json +54 -0
  210. package/tests/fixtures/management/cycle-overlap/cycles.json +39 -0
  211. package/tests/fixtures/management/cycle-overlap/invalid.json +26 -0
  212. package/tests/fixtures/management/cycle-overlap/valid.json +26 -0
  213. package/tests/fixtures/management/exists/invalid.json +13 -0
  214. package/tests/fixtures/management/exists/valid.json +25 -0
  215. package/tests/fixtures/management/fallow-dates/invalid.json +24 -0
  216. package/tests/fixtures/management/fallow-dates/valid.json +24 -0
  217. package/tests/fixtures/management/termType/invalid-cropland.json +35 -0
  218. package/tests/fixtures/management/termType/invalid-permanent-pasture.json +25 -0
  219. package/tests/fixtures/management/termType/valid-cropland.json +55 -0
  220. package/tests/fixtures/management/termType/valid-no-management.json +13 -0
  221. package/tests/fixtures/management/termType/valid-permanent-pasture.json +35 -0
  222. package/tests/fixtures/measurement/depths/invalid.json +44 -0
  223. package/tests/fixtures/measurement/depths/valid.json +50 -0
  224. package/tests/fixtures/measurement/models/valid.json +33 -0
  225. package/tests/fixtures/measurement/models/warning-no-value.json +30 -0
  226. package/tests/fixtures/measurement/models/warning.json +33 -0
  227. package/tests/fixtures/measurement/pond-measurements/invalid.json +11 -0
  228. package/tests/fixtures/measurement/pond-measurements/valid.json +23 -0
  229. package/tests/fixtures/measurement/required-depths/error.json +71 -0
  230. package/tests/fixtures/measurement/required-depths/valid.json +126 -0
  231. package/tests/fixtures/measurement/required-depths/warning.json +29 -0
  232. package/tests/fixtures/measurement/soilTexture/missing-texture-value.json +227 -0
  233. package/tests/fixtures/measurement/soilTexture/percent-invalid.json +110 -0
  234. package/tests/fixtures/measurement/soilTexture/percent-missing-value.json +43 -0
  235. package/tests/fixtures/measurement/soilTexture/percent-valid.json +110 -0
  236. package/tests/fixtures/measurement/startDate-endDate-required/invalid.json +32 -0
  237. package/tests/fixtures/measurement/startDate-endDate-required/valid.json +46 -0
  238. package/tests/fixtures/measurement/unique/invalid.json +28 -0
  239. package/tests/fixtures/measurement/unique/valid.json +16 -0
  240. package/tests/fixtures/measurement/value-length/invalid.json +46 -0
  241. package/tests/fixtures/measurement/value-length/valid.json +44 -0
  242. package/tests/fixtures/measurement/water-salinity/invalid.json +33 -0
  243. package/tests/fixtures/measurement/water-salinity/valid-brakish.json +40 -0
  244. package/tests/fixtures/measurement/water-salinity/valid.json +33 -0
  245. package/tests/fixtures/organisation/valid.json +26 -0
  246. package/tests/fixtures/practice/croppingDuration/riceGrainInHuskFlooded/invalid.json +63 -0
  247. package/tests/fixtures/practice/croppingDuration/riceGrainInHuskFlooded/valid.json +63 -0
  248. package/tests/fixtures/practice/defaultValue/invalid.json +12 -0
  249. package/tests/fixtures/practice/defaultValue/valid.json +15 -0
  250. package/tests/fixtures/practice/excretaManagement/invalid.json +50 -0
  251. package/tests/fixtures/practice/excretaManagement/valid.json +60 -0
  252. package/tests/fixtures/practice/irrigated-complete/invalid.json +47 -0
  253. package/tests/fixtures/practice/irrigated-complete/valid-incomplete.json +47 -0
  254. package/tests/fixtures/practice/irrigated-complete/valid.json +60 -0
  255. package/tests/fixtures/practice/landCover-products/invalid.json +58 -0
  256. package/tests/fixtures/practice/landCover-products/valid-coverCrop.json +69 -0
  257. package/tests/fixtures/practice/landCover-products/valid.json +58 -0
  258. package/tests/fixtures/practice/liveAnimal-system/invalid.json +58 -0
  259. package/tests/fixtures/practice/liveAnimal-system/valid.json +69 -0
  260. package/tests/fixtures/practice/longFallowDuration/invalid.json +20 -0
  261. package/tests/fixtures/practice/longFallowDuration/valid.json +20 -0
  262. package/tests/fixtures/practice/noTillage/invalid.json +23 -0
  263. package/tests/fixtures/practice/noTillage/valid-value-not-100.json +23 -0
  264. package/tests/fixtures/practice/noTillage/valid.json +21 -0
  265. package/tests/fixtures/practice/pastureGrass/key-termType/invalid.json +16 -0
  266. package/tests/fixtures/practice/pastureGrass/key-termType/valid.json +16 -0
  267. package/tests/fixtures/practice/pastureGrass/key-value/invalid-numbers.json +67 -0
  268. package/tests/fixtures/practice/pastureGrass/key-value/invalid.json +67 -0
  269. package/tests/fixtures/practice/pastureGrass/key-value/valid.json +67 -0
  270. package/tests/fixtures/practice/pastureGrass/permanent-pasture/invalid.json +37 -0
  271. package/tests/fixtures/practice/pastureGrass/permanent-pasture/valid.json +47 -0
  272. package/tests/fixtures/practice/primaryPercent/invalid.json +49 -0
  273. package/tests/fixtures/practice/primaryPercent/valid.json +49 -0
  274. package/tests/fixtures/practice/processingOperation/invalid-no-primary.json +48 -0
  275. package/tests/fixtures/practice/processingOperation/invalid.json +49 -0
  276. package/tests/fixtures/practice/processingOperation/valid-cropland.json +37 -0
  277. package/tests/fixtures/practice/processingOperation/valid.json +49 -0
  278. package/tests/fixtures/practice/productivePhasePermanentCrops/invalid.json +48 -0
  279. package/tests/fixtures/practice/productivePhasePermanentCrops/valid-0-value.json +58 -0
  280. package/tests/fixtures/practice/productivePhasePermanentCrops/valid-no-value.json +47 -0
  281. package/tests/fixtures/practice/productivePhasePermanentCrops/valid.json +48 -0
  282. package/tests/fixtures/practice/site-management/invalid.json +75 -0
  283. package/tests/fixtures/practice/site-management/valid.json +75 -0
  284. package/tests/fixtures/practice/tillage-siteType/valid.json +51 -0
  285. package/tests/fixtures/practice/tillage-siteType/warning.json +42 -0
  286. package/tests/fixtures/practice/tillage-values/invalid-fullTillage.json +61 -0
  287. package/tests/fixtures/practice/tillage-values/invalid-noTillage.json +61 -0
  288. package/tests/fixtures/practice/tillage-values/valid.json +61 -0
  289. package/tests/fixtures/practice/waterRegime/rice/invalid.json +59 -0
  290. package/tests/fixtures/practice/waterRegime/rice/valid-0-value.json +59 -0
  291. package/tests/fixtures/practice/waterRegime/rice/valid.json +58 -0
  292. package/tests/fixtures/product/economicValueShare/invalid.json +31 -0
  293. package/tests/fixtures/product/economicValueShare/valid.json +22 -0
  294. package/tests/fixtures/product/excreta/invalid.json +62 -0
  295. package/tests/fixtures/product/excreta/valid.json +62 -0
  296. package/tests/fixtures/product/excreta/warning.json +53 -0
  297. package/tests/fixtures/product/excreta/with-system/invalid.json +79 -0
  298. package/tests/fixtures/product/excreta/with-system/valid.json +88 -0
  299. package/tests/fixtures/product/excreta/with-system/warning.json +70 -0
  300. package/tests/fixtures/product/fu_ha/invalid.json +49 -0
  301. package/tests/fixtures/product/fu_ha/valid.json +49 -0
  302. package/tests/fixtures/product/primary/invalid.json +22 -0
  303. package/tests/fixtures/product/primary/valid.json +22 -0
  304. package/tests/fixtures/product/value/valid.json +26 -0
  305. package/tests/fixtures/product/value/value-0/error.json +40 -0
  306. package/tests/fixtures/product/value/value-empty/warning.json +23 -0
  307. package/tests/fixtures/product/yield/invalid.json +54 -0
  308. package/tests/fixtures/product/yield/no-value.json +75 -0
  309. package/tests/fixtures/product/yield/valid.json +54 -0
  310. package/tests/fixtures/property/default-value/valid-allowed-exception.json +61 -0
  311. package/tests/fixtures/property/default-value/valid.json +61 -0
  312. package/tests/fixtures/property/default-value/warning.json +61 -0
  313. package/tests/fixtures/property/termType/invalid.json +60 -0
  314. package/tests/fixtures/property/termType/valid.json +60 -0
  315. package/tests/fixtures/property/value-min-max/invalid.json +77 -0
  316. package/tests/fixtures/property/value-min-max/valid-skip-maximum.json +57 -0
  317. package/tests/fixtures/property/value-min-max/valid.json +78 -0
  318. package/tests/fixtures/property/valueType/invalid.json +79 -0
  319. package/tests/fixtures/property/valueType/valid.json +79 -0
  320. package/tests/fixtures/property/volatileSolidsContent/invalid.json +99 -0
  321. package/tests/fixtures/property/volatileSolidsContent/valid.json +99 -0
  322. package/tests/fixtures/shared/coordinates/invalid.json +18 -0
  323. package/tests/fixtures/shared/coordinates/valid.json +18 -0
  324. package/tests/fixtures/shared/data-duplicates/valid.json +113 -0
  325. package/tests/fixtures/shared/data-duplicates/warning.json +172 -0
  326. package/tests/fixtures/shared/duplicated-term-units/invalid-animalProduct.json +61 -0
  327. package/tests/fixtures/shared/duplicated-term-units/invalid-organicFertiliser.json +61 -0
  328. package/tests/fixtures/shared/duplicated-term-units/valid.json +49 -0
  329. package/tests/fixtures/shared/list-country-region/invalid.json +54 -0
  330. package/tests/fixtures/shared/list-country-region/valid.json +54 -0
  331. package/tests/fixtures/shared/list-percent-value/invalid.json +49 -0
  332. package/tests/fixtures/shared/list-percent-value/valid.json +52 -0
  333. package/tests/fixtures/shared/list-valueType/invalid.json +49 -0
  334. package/tests/fixtures/shared/list-valueType/valid.json +49 -0
  335. package/tests/fixtures/shared/list-values-sum-100/management/with-properties/valid.json +91 -0
  336. package/tests/fixtures/shared/list-values-sum-100/measurements/missing-soil.json +46 -0
  337. package/tests/fixtures/shared/list-values-sum-100/measurements/no-depth-high-value.json +63 -0
  338. package/tests/fixtures/shared/list-values-sum-100/measurements/no-depth-valid.json +40 -0
  339. package/tests/fixtures/shared/list-values-sum-100/measurements/with-depth-high-value.json +71 -0
  340. package/tests/fixtures/shared/list-values-sum-100/practices/total-100.json +61 -0
  341. package/tests/fixtures/shared/list-values-sum-100/practices/total-110.json +61 -0
  342. package/tests/fixtures/shared/list-values-sum-100/practices/total-90.json +61 -0
  343. package/tests/fixtures/shared/min-max/value-above.json +31 -0
  344. package/tests/fixtures/shared/min-max/value-below.json +31 -0
  345. package/tests/fixtures/shared/min-max/value-valid.json +45 -0
  346. package/tests/fixtures/shared/model/emissions/invalid.json +102 -0
  347. package/tests/fixtures/shared/model/emissions/valid-variable-tolerance.json +180 -0
  348. package/tests/fixtures/shared/model/emissions/valid.json +102 -0
  349. package/tests/fixtures/shared/model/impacts/invalid.json +75 -0
  350. package/tests/fixtures/shared/model/impacts/valid.json +75 -0
  351. package/tests/fixtures/shared/model/inputs/valid-no-value.json +84 -0
  352. package/tests/fixtures/shared/model/inputs/valid.json +87 -0
  353. package/tests/fixtures/shared/model/inputs/warning.json +87 -0
  354. package/tests/fixtures/shared/model/products/valid-no-value.json +88 -0
  355. package/tests/fixtures/shared/model/products/valid.json +91 -0
  356. package/tests/fixtures/shared/model/products/warning.json +91 -0
  357. package/tests/fixtures/shared/otherModel/invalid.json +69 -0
  358. package/tests/fixtures/shared/otherModel/valid.json +70 -0
  359. package/tests/fixtures/shared/properties-duplicate-values/invalid.json +61 -0
  360. package/tests/fixtures/shared/properties-duplicate-values/valid.json +57 -0
  361. package/tests/fixtures/shared/properties-same-length/invalid.json +62 -0
  362. package/tests/fixtures/shared/properties-same-length/valid.json +52 -0
  363. package/tests/fixtures/shared/unit-percent/invalid.json +34 -0
  364. package/tests/fixtures/shared/unit-percent/valid.json +60 -0
  365. package/tests/fixtures/shared/unit-percent/warning.json +52 -0
  366. package/tests/fixtures/site/cycles-linked-ia/invalid.json +129 -0
  367. package/tests/fixtures/site/cycles-linked-ia/valid.json +129 -0
  368. package/tests/fixtures/site/valid.json +138 -0
  369. package/tests/fixtures/source/valid.json +19 -0
  370. package/tests/fixtures/transformation/excretaManagement/invalid.json +47 -0
  371. package/tests/fixtures/transformation/excretaManagement/valid.json +59 -0
  372. package/tests/fixtures/transformation/inputs-products/invalid.json +43 -0
  373. package/tests/fixtures/transformation/inputs-products/valid.json +43 -0
  374. package/tests/fixtures/transformation/linked-emission/invalid.json +101 -0
  375. package/tests/fixtures/transformation/linked-emission/valid.json +107 -0
  376. package/tests/fixtures/transformation/previousTransformationId/invalid-no-previous.json +127 -0
  377. package/tests/fixtures/transformation/previousTransformationId/invalid-previous-input.json +100 -0
  378. package/tests/fixtures/transformation/previousTransformationId/invalid-product-input.json +106 -0
  379. package/tests/fixtures/transformation/previousTransformationId/invalid-wrong-order.json +136 -0
  380. package/tests/fixtures/transformation/previousTransformationId/valid.json +171 -0
  381. package/tests/integration/__init__.py +0 -0
  382. package/tests/integration/test_product.py +17 -0
  383. package/tests/test_gee.py +10 -0
  384. package/tests/test_utils.py +36 -0
  385. package/tests/test_validation.py +11 -0
  386. package/tests/utils.py +28 -0
  387. package/tests/validators/__init__.py +0 -0
  388. package/tests/validators/test_aggregated_cycle.py +44 -0
  389. package/tests/validators/test_aggregated_shared.py +63 -0
  390. package/tests/validators/test_animal.py +72 -0
  391. package/tests/validators/test_completeness.py +337 -0
  392. package/tests/validators/test_cycle.py +600 -0
  393. package/tests/validators/test_emission.py +170 -0
  394. package/tests/validators/test_impact_assessment.py +80 -0
  395. package/tests/validators/test_indicator.py +120 -0
  396. package/tests/validators/test_infrastructure.py +26 -0
  397. package/tests/validators/test_input.py +434 -0
  398. package/tests/validators/test_management.py +177 -0
  399. package/tests/validators/test_measurement.py +317 -0
  400. package/tests/validators/test_organisation.py +32 -0
  401. package/tests/validators/test_practice.py +490 -0
  402. package/tests/validators/test_product.py +291 -0
  403. package/tests/validators/test_property.py +143 -0
  404. package/tests/validators/test_shared.py +1139 -0
  405. package/tests/validators/test_site.py +151 -0
  406. package/tests/validators/test_source.py +15 -0
  407. package/tests/validators/test_transformation.py +151 -0
  408. package/tests/validators/test_validators.py +74 -0
  409. package/tsconfig.dist.json +9 -0
  410. 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
+ )