@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,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
+ )