hestia-earth-models 0.74.8__py3-none-any.whl → 0.74.10__py3-none-any.whl
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.
Potentially problematic release.
This version of hestia-earth-models might be problematic. Click here for more details.
- hestia_earth/models/cache_sites.py +1 -1
- hestia_earth/models/faostat2018/liveweightPerHead.py +1 -1
- hestia_earth/models/faostat2018/product/price.py +1 -1
- hestia_earth/models/geospatialDatabase/ecoClimateZone.py +1 -1
- hestia_earth/models/geospatialDatabase/region.py +1 -1
- hestia_earth/models/geospatialDatabase/utils.py +1 -1
- hestia_earth/models/globalCropWaterModel2008/rootingDepth.py +2 -1
- hestia_earth/models/haversineFormula/transport/distance.py +1 -1
- hestia_earth/models/hestia/aboveGroundCropResidue.py +1 -3
- hestia_earth/models/hestia/cropResidueManagement.py +1 -0
- hestia_earth/models/hestia/excretaKgMass.py +1 -1
- hestia_earth/models/hestia/landCover.py +13 -6
- hestia_earth/models/hestia/landOccupationDuringCycle.py +1 -1
- hestia_earth/models/hestia/management.py +25 -11
- hestia_earth/models/hestia/pastureGrass.py +1 -1
- hestia_earth/models/impact_assessment/post_checks/__init__.py +3 -2
- hestia_earth/models/impact_assessment/post_checks/remove_no_value.py +13 -0
- hestia_earth/models/ipcc2019/biocharOrganicCarbonPerHa.py +2 -1
- hestia_earth/models/ipcc2019/ch4ToAirExcreta.py +16 -13
- hestia_earth/models/ipcc2019/organicCarbonPerHa.py +5 -1
- hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_1.py +88 -101
- hestia_earth/models/ipcc2019/organicCarbonPerHa_utils.py +21 -0
- hestia_earth/models/mocking/search-results.json +1 -1
- hestia_earth/models/site/pre_checks/country.py +1 -2
- hestia_earth/models/utils/__init__.py +7 -5
- hestia_earth/models/utils/blank_node.py +7 -2
- hestia_earth/models/utils/completeness.py +1 -2
- hestia_earth/models/utils/emission.py +1 -1
- hestia_earth/models/utils/indicator.py +1 -1
- hestia_earth/models/utils/input.py +1 -1
- hestia_earth/models/utils/management.py +1 -1
- hestia_earth/models/utils/measurement.py +2 -1
- hestia_earth/models/utils/method.py +1 -2
- hestia_earth/models/utils/practice.py +1 -1
- hestia_earth/models/utils/product.py +2 -1
- hestia_earth/models/utils/property.py +2 -1
- hestia_earth/models/utils/term.py +1 -27
- hestia_earth/models/version.py +1 -1
- {hestia_earth_models-0.74.8.dist-info → hestia_earth_models-0.74.10.dist-info}/METADATA +2 -2
- {hestia_earth_models-0.74.8.dist-info → hestia_earth_models-0.74.10.dist-info}/RECORD +49 -46
- tests/models/hestia/test_aboveGroundCropResidue.py +13 -35
- tests/models/hestia/test_landOccupationDuringCycle.py +9 -2
- tests/models/impact_assessment/post_checks/test_remove_cache_fields.py +6 -0
- tests/models/impact_assessment/post_checks/test_remove_no_value.py +17 -0
- tests/models/ipcc2019/test_ch4ToAirExcreta.py +0 -12
- tests/models/ipcc2019/test_organicCarbonPerHa_tier_1.py +1 -1
- {hestia_earth_models-0.74.8.dist-info → hestia_earth_models-0.74.10.dist-info}/LICENSE +0 -0
- {hestia_earth_models-0.74.8.dist-info → hestia_earth_models-0.74.10.dist-info}/WHEEL +0 -0
- {hestia_earth_models-0.74.8.dist-info → hestia_earth_models-0.74.10.dist-info}/top_level.txt +0 -0
|
@@ -19,14 +19,16 @@ from hestia_earth.models.utils.blank_node import (
|
|
|
19
19
|
from hestia_earth.models.utils.ecoClimateZone import EcoClimateZone, get_eco_climate_zone_value
|
|
20
20
|
from hestia_earth.models.utils.measurement import _new_measurement
|
|
21
21
|
from hestia_earth.models.utils.property import get_node_property
|
|
22
|
-
from hestia_earth.models.utils.term import
|
|
22
|
+
from hestia_earth.models.utils.term import (
|
|
23
|
+
get_lookup_value, get_residue_removed_or_burnt_terms, get_upland_rice_land_cover_terms
|
|
24
|
+
)
|
|
23
25
|
|
|
24
26
|
from .organicCarbonPerHa_utils import (
|
|
25
|
-
check_irrigation, DEPTH_LOWER, DEPTH_UPPER, IPCC_SOIL_CATEGORY_TO_SOIL_TYPE_LOOKUP_VALUE,
|
|
27
|
+
check_irrigation, DEPTH_LOWER, DEPTH_UPPER, format_soil_inventory, IPCC_SOIL_CATEGORY_TO_SOIL_TYPE_LOOKUP_VALUE,
|
|
26
28
|
IPCC_LAND_USE_CATEGORY_TO_LAND_COVER_LOOKUP_VALUE, IPCC_MANAGEMENT_CATEGORY_TO_GRASSLAND_MANAGEMENT_TERM_ID,
|
|
27
29
|
IPCC_MANAGEMENT_CATEGORY_TO_TILLAGE_MANAGEMENT_LOOKUP_VALUE, IpccSoilCategory, IpccCarbonInputCategory,
|
|
28
30
|
IpccLandUseCategory, IpccManagementCategory, is_cover_crop, MIN_AREA_THRESHOLD, sample_constant,
|
|
29
|
-
sample_plus_minus_error, sample_plus_minus_uncertainty, SITE_TYPE_TO_IPCC_LAND_USE_CATEGORY,
|
|
31
|
+
sample_plus_minus_error, sample_plus_minus_uncertainty, SITE_TYPE_TO_IPCC_LAND_USE_CATEGORY, SoilData,
|
|
30
32
|
SUPER_MAJORITY_AREA_THRESHOLD, STATS_DEFINITION
|
|
31
33
|
)
|
|
32
34
|
from . import MODEL
|
|
@@ -45,7 +47,11 @@ REQUIREMENTS = {
|
|
|
45
47
|
],
|
|
46
48
|
"optional": {
|
|
47
49
|
"measurements": [
|
|
48
|
-
{
|
|
50
|
+
{
|
|
51
|
+
"@doc": "This model cannot run on sites with more than 30 percent organic soils (`histols`, `histosol` and their subclasses).", # noqa: E501
|
|
52
|
+
"@type": "Measurement", "value": "",
|
|
53
|
+
"term.termType": ["soilType", "usdaSoilType"]
|
|
54
|
+
}
|
|
49
55
|
],
|
|
50
56
|
"management": [
|
|
51
57
|
{
|
|
@@ -606,16 +612,19 @@ def should_run(site: dict) -> tuple[bool, dict, dict]:
|
|
|
606
612
|
measurement_nodes = site.get("measurements", [])
|
|
607
613
|
|
|
608
614
|
eco_climate_zone = get_eco_climate_zone_value(site, as_enum=True)
|
|
609
|
-
ipcc_soil_category = _assign_ipcc_soil_category(measurement_nodes)
|
|
615
|
+
ipcc_soil_category, soil_logs = _assign_ipcc_soil_category(measurement_nodes)
|
|
610
616
|
soc_ref = _get_soc_ref_preview(ipcc_soil_category, eco_climate_zone)
|
|
611
617
|
|
|
618
|
+
valid_site_type = site_type in _VALID_SITE_TYPES
|
|
619
|
+
valid_eco_climate_zone = eco_climate_zone not in _EXCLUDED_ECO_CLIMATE_ZONES
|
|
620
|
+
valid_soc_ref = isinstance(soc_ref, (float, int)) and soc_ref > 0
|
|
612
621
|
has_management = len(management_nodes) > 0
|
|
613
622
|
has_measurements = len(measurement_nodes) > 0
|
|
614
623
|
|
|
615
624
|
should_compile_inventory = all([
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
625
|
+
valid_site_type,
|
|
626
|
+
valid_eco_climate_zone,
|
|
627
|
+
valid_soc_ref,
|
|
619
628
|
has_management,
|
|
620
629
|
has_measurements
|
|
621
630
|
])
|
|
@@ -638,9 +647,13 @@ def should_run(site: dict) -> tuple[bool, dict, dict]:
|
|
|
638
647
|
year for year, group in inventory.items() if group.get(_InventoryKey.SHOULD_RUN)
|
|
639
648
|
)
|
|
640
649
|
|
|
641
|
-
logs = inventory_logs | {
|
|
650
|
+
logs = soil_logs | inventory_logs | {
|
|
642
651
|
"site_type": site_type,
|
|
652
|
+
"soc_ref_available": valid_soc_ref,
|
|
643
653
|
"soc_ref": soc_ref,
|
|
654
|
+
"valid_eco_climate_zone": valid_eco_climate_zone,
|
|
655
|
+
"valid_soil_category": ipcc_soil_category not in [IpccSoilCategory.ORGANIC_SOILS],
|
|
656
|
+
"valid_site_type": valid_site_type,
|
|
644
657
|
"has_management": has_management,
|
|
645
658
|
"has_measurements": has_measurements,
|
|
646
659
|
"should_compile_inventory_tier_1": should_compile_inventory,
|
|
@@ -1034,24 +1047,34 @@ def _assign_ipcc_soil_category(
|
|
|
1034
1047
|
soil_types = _get_soil_type_measurements(measurement_nodes, TermTermType.SOILTYPE)
|
|
1035
1048
|
usda_soil_types = _get_soil_type_measurements(measurement_nodes, TermTermType.USDASOILTYPE)
|
|
1036
1049
|
|
|
1050
|
+
soil_data = [_unpack_soil_data(node) for node in soil_types]
|
|
1051
|
+
usda_soil_data = [_unpack_soil_data(node) for node in usda_soil_types]
|
|
1052
|
+
|
|
1037
1053
|
clay_content = get_node_value(find_term_match(measurement_nodes, _CLAY_CONTENT_TERM_ID))
|
|
1038
1054
|
sand_content = get_node_value(find_term_match(measurement_nodes, _SAND_CONTENT_TERM_ID))
|
|
1039
|
-
|
|
1040
1055
|
has_sandy_soil = clay_content < _CLAY_CONTENT_MAX and sand_content > _SAND_CONTENT_MIN
|
|
1041
1056
|
|
|
1042
|
-
|
|
1057
|
+
logs = {
|
|
1058
|
+
"soil_data": format_soil_inventory(soil_data),
|
|
1059
|
+
"usda_soil_data": format_soil_inventory(usda_soil_data),
|
|
1060
|
+
"has_sandy_soil_texture": has_sandy_soil
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
category = next(
|
|
1043
1064
|
(
|
|
1044
1065
|
key for key in _SOIL_CATEGORY_DECISION_TREE
|
|
1045
|
-
if
|
|
1066
|
+
if _check_soil_category(
|
|
1046
1067
|
key=key,
|
|
1047
|
-
|
|
1048
|
-
|
|
1068
|
+
soil_data=soil_data,
|
|
1069
|
+
usda_soil_data=usda_soil_data,
|
|
1049
1070
|
has_sandy_soil=has_sandy_soil
|
|
1050
1071
|
)
|
|
1051
1072
|
),
|
|
1052
1073
|
default
|
|
1053
1074
|
) if len(soil_types) > 0 or len(usda_soil_types) > 0 else default
|
|
1054
1075
|
|
|
1076
|
+
return category, logs
|
|
1077
|
+
|
|
1055
1078
|
|
|
1056
1079
|
def _get_soil_type_measurements(
|
|
1057
1080
|
nodes: list[dict], term_type: Literal[TermTermType.SOILTYPE, TermTermType.USDASOILTYPE]
|
|
@@ -1067,98 +1090,66 @@ def _get_soil_type_measurements(
|
|
|
1067
1090
|
)
|
|
1068
1091
|
|
|
1069
1092
|
|
|
1070
|
-
def
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
**_
|
|
1076
|
-
) -> bool:
|
|
1077
|
-
"""
|
|
1078
|
-
Check if the soil category matches the given key.
|
|
1079
|
-
|
|
1080
|
-
Parameters
|
|
1081
|
-
----------
|
|
1082
|
-
key : IpccSoilCategory
|
|
1083
|
-
The IPCC soil category to check.
|
|
1084
|
-
soil_types : list[dict]
|
|
1085
|
-
List of soil type measurement nodes.
|
|
1086
|
-
usda_soil_types : list[dict]
|
|
1087
|
-
List of USDA soil type measurement nodes
|
|
1088
|
-
|
|
1089
|
-
Returns
|
|
1090
|
-
-------
|
|
1091
|
-
bool
|
|
1092
|
-
`True` if the soil category matches, `False` otherwise.
|
|
1093
|
-
"""
|
|
1094
|
-
SOIL_TYPE_LOOKUP = LOOKUPS["soilType"]
|
|
1095
|
-
USDA_SOIL_TYPE_LOOKUP = LOOKUPS["usdaSoilType"]
|
|
1093
|
+
def _unpack_soil_data(node):
|
|
1094
|
+
term = node.get("term", {})
|
|
1095
|
+
term_id = term.get("@id")
|
|
1096
|
+
term_type = term.get("termType")
|
|
1097
|
+
value = get_node_value(node)
|
|
1096
1098
|
|
|
1097
|
-
|
|
1099
|
+
lookup_value = get_lookup_value(term, LOOKUPS[term_type]) if term_type else None
|
|
1100
|
+
category = next(key for key, value in IPCC_SOIL_CATEGORY_TO_SOIL_TYPE_LOOKUP_VALUE.items() if value == lookup_value)
|
|
1098
1101
|
|
|
1099
|
-
|
|
1100
|
-
soil_types,
|
|
1101
|
-
lookup=SOIL_TYPE_LOOKUP,
|
|
1102
|
-
target_lookup_values=target_lookup_values,
|
|
1103
|
-
cumulative_threshold=MIN_AREA_THRESHOLD
|
|
1104
|
-
)
|
|
1102
|
+
return SoilData(term_id, value, category)
|
|
1105
1103
|
|
|
1106
|
-
is_usda_soil_type_match = cumulative_nodes_lookup_match(
|
|
1107
|
-
usda_soil_types,
|
|
1108
|
-
lookup=USDA_SOIL_TYPE_LOOKUP,
|
|
1109
|
-
target_lookup_values=target_lookup_values,
|
|
1110
|
-
cumulative_threshold=MIN_AREA_THRESHOLD
|
|
1111
|
-
)
|
|
1112
1104
|
|
|
1113
|
-
|
|
1105
|
+
_IPCC_SOIL_CATEGORY_TO_OVERRIDE_KWARGS = {
|
|
1106
|
+
IpccSoilCategory.SANDY_SOILS: {"has_sandy_soil"}
|
|
1107
|
+
}
|
|
1108
|
+
"""
|
|
1109
|
+
Keyword arguments that can override the `soilType`/`usdaSoilType` lookup match for an `IpccSoilCategory`.
|
|
1110
|
+
"""
|
|
1114
1111
|
|
|
1115
1112
|
|
|
1116
|
-
def
|
|
1117
|
-
*,
|
|
1118
|
-
key: IpccSoilCategory,
|
|
1119
|
-
soil_types: list[dict],
|
|
1120
|
-
usda_soil_types: list[dict],
|
|
1121
|
-
has_sandy_soil: bool,
|
|
1122
|
-
**_
|
|
1113
|
+
def _check_soil_category(
|
|
1114
|
+
*, key: IpccSoilCategory, soil_data: list[SoilData], usda_soil_data: list[SoilData], **kwargs
|
|
1123
1115
|
) -> bool:
|
|
1124
1116
|
"""
|
|
1125
|
-
Check if the
|
|
1126
|
-
|
|
1127
|
-
This function is special case of `_check_soil_category`.
|
|
1117
|
+
Check if the soil category matches the given key.
|
|
1128
1118
|
|
|
1129
1119
|
Parameters
|
|
1130
1120
|
----------
|
|
1131
1121
|
key : IpccSoilCategory
|
|
1132
1122
|
The IPCC soil category to check.
|
|
1133
|
-
|
|
1134
|
-
List of
|
|
1135
|
-
|
|
1136
|
-
List of
|
|
1137
|
-
has_sandy_soil : bool
|
|
1138
|
-
True if the soils are sandy, False otherwise.
|
|
1123
|
+
soil_data : list[SoilData]
|
|
1124
|
+
List of `SoilData` NamedEnums generated from `soilType` measurement nodes.
|
|
1125
|
+
usda_soil_data : list[SoilData]
|
|
1126
|
+
List of `SoilData` NamedEnums generated from `usdaSoilType` measurement nodes.
|
|
1139
1127
|
|
|
1140
1128
|
Returns
|
|
1141
1129
|
-------
|
|
1142
1130
|
bool
|
|
1143
1131
|
`True` if the soil category matches, `False` otherwise.
|
|
1144
1132
|
"""
|
|
1145
|
-
|
|
1133
|
+
override_kwargs = _IPCC_SOIL_CATEGORY_TO_OVERRIDE_KWARGS.get(key, set())
|
|
1134
|
+
valid_override = any(v for k, v in kwargs.items() if k in override_kwargs)
|
|
1146
1135
|
|
|
1136
|
+
is_soil_match = sum(data.value for data in soil_data if data.category == key) > MIN_AREA_THRESHOLD
|
|
1137
|
+
is_usda_soil_match = sum(data.value for data in usda_soil_data if data.category == key) > MIN_AREA_THRESHOLD
|
|
1138
|
+
|
|
1139
|
+
return valid_override or is_soil_match or is_usda_soil_match
|
|
1147
1140
|
|
|
1148
|
-
_SOIL_CATEGORY_DECISION_TREE = {
|
|
1149
|
-
IpccSoilCategory.ORGANIC_SOILS: _check_soil_category,
|
|
1150
|
-
IpccSoilCategory.SANDY_SOILS: _check_sandy_soil_category,
|
|
1151
|
-
IpccSoilCategory.WETLAND_SOILS: _check_soil_category,
|
|
1152
|
-
IpccSoilCategory.VOLCANIC_SOILS: _check_soil_category,
|
|
1153
|
-
IpccSoilCategory.SPODIC_SOILS: _check_soil_category,
|
|
1154
|
-
IpccSoilCategory.HIGH_ACTIVITY_CLAY_SOILS: _check_soil_category,
|
|
1155
|
-
IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS: _check_soil_category
|
|
1156
|
-
}
|
|
1157
|
-
"""
|
|
1158
|
-
A decision tree mapping IPCC soil categories to corresponding check functions.
|
|
1159
1141
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1142
|
+
_SOIL_CATEGORY_DECISION_TREE = [
|
|
1143
|
+
IpccSoilCategory.ORGANIC_SOILS,
|
|
1144
|
+
IpccSoilCategory.SANDY_SOILS,
|
|
1145
|
+
IpccSoilCategory.WETLAND_SOILS,
|
|
1146
|
+
IpccSoilCategory.VOLCANIC_SOILS,
|
|
1147
|
+
IpccSoilCategory.SPODIC_SOILS,
|
|
1148
|
+
IpccSoilCategory.HIGH_ACTIVITY_CLAY_SOILS,
|
|
1149
|
+
IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS
|
|
1150
|
+
]
|
|
1151
|
+
"""
|
|
1152
|
+
A decision tree determining the order to check IPCC soil categories.
|
|
1162
1153
|
"""
|
|
1163
1154
|
|
|
1164
1155
|
|
|
@@ -1201,7 +1192,7 @@ def _assign_ipcc_land_use_category(
|
|
|
1201
1192
|
return next(
|
|
1202
1193
|
(
|
|
1203
1194
|
key for key in DECISION_TREE
|
|
1204
|
-
if
|
|
1195
|
+
if _check_ipcc_land_use_category(
|
|
1205
1196
|
key=key,
|
|
1206
1197
|
land_cover_nodes=land_cover_nodes,
|
|
1207
1198
|
has_long_fallow=has_long_fallow,
|
|
@@ -1317,23 +1308,19 @@ Keyword arguments that can override the `landCover` lookup match for specific `I
|
|
|
1317
1308
|
"""
|
|
1318
1309
|
|
|
1319
1310
|
|
|
1320
|
-
_LAND_USE_CATEGORY_DECISION_TREE =
|
|
1321
|
-
IpccLandUseCategory.GRASSLAND
|
|
1322
|
-
IpccLandUseCategory.SET_ASIDE
|
|
1323
|
-
IpccLandUseCategory.PERENNIAL_CROPS
|
|
1324
|
-
IpccLandUseCategory.PADDY_RICE_CULTIVATION
|
|
1325
|
-
IpccLandUseCategory.ANNUAL_CROPS_WET
|
|
1326
|
-
IpccLandUseCategory.ANNUAL_CROPS
|
|
1327
|
-
IpccLandUseCategory.FOREST
|
|
1328
|
-
IpccLandUseCategory.NATIVE
|
|
1329
|
-
IpccLandUseCategory.OTHER
|
|
1330
|
-
|
|
1311
|
+
_LAND_USE_CATEGORY_DECISION_TREE = [
|
|
1312
|
+
IpccLandUseCategory.GRASSLAND,
|
|
1313
|
+
IpccLandUseCategory.SET_ASIDE,
|
|
1314
|
+
IpccLandUseCategory.PERENNIAL_CROPS,
|
|
1315
|
+
IpccLandUseCategory.PADDY_RICE_CULTIVATION,
|
|
1316
|
+
IpccLandUseCategory.ANNUAL_CROPS_WET,
|
|
1317
|
+
IpccLandUseCategory.ANNUAL_CROPS,
|
|
1318
|
+
IpccLandUseCategory.FOREST,
|
|
1319
|
+
IpccLandUseCategory.NATIVE,
|
|
1320
|
+
IpccLandUseCategory.OTHER
|
|
1321
|
+
]
|
|
1331
1322
|
"""
|
|
1332
|
-
A decision tree
|
|
1333
|
-
|
|
1334
|
-
Key: IpccLandUseCategory
|
|
1335
|
-
Value: Corresponding function for checking the match of the given land use category based on land cover nodes
|
|
1336
|
-
and additional kwargs.
|
|
1323
|
+
A decision tree determining the order to check IPCC land use categories.
|
|
1337
1324
|
"""
|
|
1338
1325
|
|
|
1339
1326
|
|
|
@@ -8,6 +8,7 @@ from hestia_earth.utils.stats import calc_z_critical
|
|
|
8
8
|
from hestia_earth.utils.stats import (
|
|
9
9
|
repeat_single, truncated_normal_1d
|
|
10
10
|
)
|
|
11
|
+
from hestia_earth.models.log import log_as_table
|
|
11
12
|
from hestia_earth.models.utils.blank_node import cumulative_nodes_term_match, node_term_match
|
|
12
13
|
from hestia_earth.models.utils.term import get_cover_crop_property_terms, get_irrigated_terms
|
|
13
14
|
|
|
@@ -178,6 +179,16 @@ lignin_content : float
|
|
|
178
179
|
"""
|
|
179
180
|
|
|
180
181
|
|
|
182
|
+
SoilData = NamedTuple(
|
|
183
|
+
"SoilData",
|
|
184
|
+
[
|
|
185
|
+
("term_id", str),
|
|
186
|
+
("value", float),
|
|
187
|
+
("category", IpccSoilCategory)
|
|
188
|
+
]
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
181
192
|
def check_consecutive(ints: list[int]) -> bool:
|
|
182
193
|
"""
|
|
183
194
|
Checks whether a list of integers are consecutive.
|
|
@@ -286,3 +297,13 @@ def format_number_list(values: Optional[list[float]]) -> str:
|
|
|
286
297
|
" ".join(format_number(value) for value in values) or "None"if isinstance(values, list)
|
|
287
298
|
else "None"
|
|
288
299
|
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def format_soil_inventory(inventory: list[SoilData]) -> str:
|
|
303
|
+
return log_as_table(
|
|
304
|
+
{
|
|
305
|
+
"term-id": data.term_id,
|
|
306
|
+
"value": format_number(data.value),
|
|
307
|
+
"category": format_enum(data.category)
|
|
308
|
+
} for data in inventory
|
|
309
|
+
) if inventory else "None"
|