hestia-earth-models 0.65.5__py3-none-any.whl → 0.65.7__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.

Files changed (57) hide show
  1. hestia_earth/models/cml2001Baseline/abioticResourceDepletionFossilFuels.py +9 -5
  2. hestia_earth/models/config/Cycle.json +2193 -0
  3. hestia_earth/models/config/ImpactAssessment.json +2041 -0
  4. hestia_earth/models/config/Site.json +471 -0
  5. hestia_earth/models/config/__init__.py +71 -0
  6. hestia_earth/models/config/run-calculations.json +42 -0
  7. hestia_earth/models/config/trigger-calculations.json +43 -0
  8. hestia_earth/models/hestia/landCover.py +70 -22
  9. hestia_earth/models/ipcc2019/animal/hoursWorkedPerDay.py +38 -0
  10. hestia_earth/models/mocking/search-results.json +833 -829
  11. hestia_earth/models/site/management.py +82 -22
  12. hestia_earth/models/utils/crop.py +5 -1
  13. hestia_earth/models/version.py +1 -1
  14. hestia_earth/orchestrator/__init__.py +40 -0
  15. hestia_earth/orchestrator/log.py +62 -0
  16. hestia_earth/orchestrator/models/__init__.py +118 -0
  17. hestia_earth/orchestrator/models/emissions/__init__.py +0 -0
  18. hestia_earth/orchestrator/models/emissions/deleted.py +15 -0
  19. hestia_earth/orchestrator/models/transformations.py +103 -0
  20. hestia_earth/orchestrator/strategies/__init__.py +0 -0
  21. hestia_earth/orchestrator/strategies/merge/__init__.py +42 -0
  22. hestia_earth/orchestrator/strategies/merge/merge_append.py +29 -0
  23. hestia_earth/orchestrator/strategies/merge/merge_default.py +1 -0
  24. hestia_earth/orchestrator/strategies/merge/merge_list.py +103 -0
  25. hestia_earth/orchestrator/strategies/merge/merge_node.py +59 -0
  26. hestia_earth/orchestrator/strategies/run/__init__.py +8 -0
  27. hestia_earth/orchestrator/strategies/run/add_blank_node_if_missing.py +85 -0
  28. hestia_earth/orchestrator/strategies/run/add_key_if_missing.py +9 -0
  29. hestia_earth/orchestrator/strategies/run/always.py +6 -0
  30. hestia_earth/orchestrator/utils.py +116 -0
  31. {hestia_earth_models-0.65.5.dist-info → hestia_earth_models-0.65.7.dist-info}/METADATA +27 -5
  32. {hestia_earth_models-0.65.5.dist-info → hestia_earth_models-0.65.7.dist-info}/RECORD +57 -13
  33. tests/models/cml2001Baseline/test_abioticResourceDepletionFossilFuels.py +1 -1
  34. tests/models/hestia/test_landCover.py +2 -1
  35. tests/models/ipcc2019/animal/test_hoursWorkedPerDay.py +22 -0
  36. tests/models/test_config.py +115 -0
  37. tests/orchestrator/__init__.py +0 -0
  38. tests/orchestrator/models/__init__.py +0 -0
  39. tests/orchestrator/models/emissions/__init__.py +0 -0
  40. tests/orchestrator/models/emissions/test_deleted.py +21 -0
  41. tests/orchestrator/models/test_transformations.py +29 -0
  42. tests/orchestrator/strategies/__init__.py +0 -0
  43. tests/orchestrator/strategies/merge/__init__.py +0 -0
  44. tests/orchestrator/strategies/merge/test_merge_append.py +33 -0
  45. tests/orchestrator/strategies/merge/test_merge_default.py +7 -0
  46. tests/orchestrator/strategies/merge/test_merge_list.py +327 -0
  47. tests/orchestrator/strategies/merge/test_merge_node.py +95 -0
  48. tests/orchestrator/strategies/run/__init__.py +0 -0
  49. tests/orchestrator/strategies/run/test_add_blank_node_if_missing.py +114 -0
  50. tests/orchestrator/strategies/run/test_add_key_if_missing.py +14 -0
  51. tests/orchestrator/strategies/run/test_always.py +5 -0
  52. tests/orchestrator/test_models.py +69 -0
  53. tests/orchestrator/test_orchestrator.py +27 -0
  54. tests/orchestrator/test_utils.py +109 -0
  55. {hestia_earth_models-0.65.5.dist-info → hestia_earth_models-0.65.7.dist-info}/LICENSE +0 -0
  56. {hestia_earth_models-0.65.5.dist-info → hestia_earth_models-0.65.7.dist-info}/WHEEL +0 -0
  57. {hestia_earth_models-0.65.5.dist-info → hestia_earth_models-0.65.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,22 @@
1
+ from unittest.mock import patch
2
+ import json
3
+ from tests.utils import fixtures_path, fake_new_property
4
+
5
+ from hestia_earth.models.ipcc2019 import MODEL
6
+ from hestia_earth.models.ipcc2019.animal.hoursWorkedPerDay import TERM_ID, run
7
+
8
+ utils_path = '.'.join(['hestia_earth', 'models', MODEL, 'animal', 'utils'])
9
+ class_path = f"hestia_earth.models.{MODEL}.animal.{TERM_ID}"
10
+ fixtures_folder = f"{fixtures_path}/{MODEL}/animal/{TERM_ID}"
11
+
12
+
13
+ @patch(f"{utils_path}._new_property", side_effect=fake_new_property)
14
+ def test_run(*args):
15
+ with open(f"{fixtures_folder}/cycle.jsonld", encoding='utf-8') as f:
16
+ cycle = json.load(f)
17
+
18
+ with open(f"{fixtures_folder}/result.jsonld", encoding='utf-8') as f:
19
+ expected = json.load(f)
20
+
21
+ value = run(cycle)
22
+ assert value == expected
@@ -0,0 +1,115 @@
1
+ import pytest
2
+ import importlib
3
+ from hestia_earth.utils.tools import flatten
4
+
5
+ from hestia_earth.models.config import (
6
+ load_config, config_max_stage, _is_aggregated_model, _remove_aggregated, load_run_config, load_trigger_config
7
+ )
8
+
9
+
10
+ _aggregated_model = {
11
+ "value": "input.hestiaAggregatedData"
12
+ }
13
+ _other_model = {
14
+ "value": "otherModel"
15
+ }
16
+
17
+
18
+ def test_load_config():
19
+ node_type = 'Cycle'
20
+ config = load_config(node_type)
21
+ assert config.get('models') is not None
22
+
23
+
24
+ def test_load_config_error():
25
+ node_type = 'Unkown'
26
+
27
+ with pytest.raises(Exception, match='Invalid type Unkown.'):
28
+ load_config(node_type)
29
+
30
+
31
+ def test_config_max_stage():
32
+ node_type = 'Cycle'
33
+ config = load_config(node_type)
34
+ assert config_max_stage(config) == 2
35
+
36
+
37
+ def test_is_aggregated_model():
38
+ assert _is_aggregated_model(_aggregated_model) is True
39
+ assert not _is_aggregated_model(_other_model)
40
+
41
+
42
+ def test_remove_aggregated():
43
+ models = [
44
+ [_aggregated_model, _other_model],
45
+ _aggregated_model, _other_model
46
+ ]
47
+ assert _remove_aggregated(models) == [[_other_model], _other_model]
48
+
49
+
50
+ def test_load_config_skip_aggregated_models():
51
+ node_type = 'Cycle'
52
+ all_models = load_config(node_type, skip_aggregated_models=False).get('models')
53
+ models_no_aggregated = load_config(node_type, skip_aggregated_models=True).get('models')
54
+ assert all_models != models_no_aggregated
55
+
56
+
57
+ def test_load_run_config():
58
+ assert len(load_run_config(node_type='Site', stage=1)) == 0
59
+ assert len(load_run_config(node_type='Site', stage=2)) == 1
60
+
61
+
62
+ def test_load_run_config_invalid_stage():
63
+ with pytest.raises(Exception) as e:
64
+ load_run_config(
65
+ node_type='ImpactAssessment',
66
+ stage=2
67
+ )
68
+ assert str(e.value) == 'Invalid stage configuration for ImpactAssessment: 2'
69
+
70
+
71
+ def test_load_trigger_config_config():
72
+ assert len(load_trigger_config(node_type='Site', stage=1)) == 1
73
+ assert len(load_trigger_config(node_type='Site', stage=2)) == 1
74
+
75
+
76
+ def test_load_trigger_config_invalid_stage():
77
+ with pytest.raises(Exception) as e:
78
+ load_trigger_config(
79
+ node_type='ImpactAssessment',
80
+ stage=2
81
+ )
82
+ assert str(e.value) == 'Invalid stage configuration for ImpactAssessment: 2'
83
+
84
+
85
+ # included in orchestrator
86
+ _ignore_models = ['emissions.deleted', 'transformations']
87
+ _ignore_values = [None, '', 'all']
88
+
89
+
90
+ def _model_path(model: dict):
91
+ name = model.get('model')
92
+ value = model.get('value')
93
+ suffix = f"hestia_earth.models.{name}"
94
+ return f"{suffix}.{value}" if value not in _ignore_values else suffix
95
+
96
+
97
+ def _get_models_paths(node_type: str):
98
+ models = flatten(load_config(node_type).get('models', []))
99
+ return [
100
+ _model_path(m)
101
+ for m in models
102
+ if m.get('model') not in _ignore_models
103
+ ]
104
+
105
+
106
+ @pytest.mark.parametrize(
107
+ 'node_type',
108
+ ['Cycle', 'Site', 'ImpactAssessment']
109
+ )
110
+ def test_load_config_cycle(node_type):
111
+ paths = _get_models_paths(node_type)
112
+
113
+ for path in paths:
114
+ run = importlib.import_module(path).run
115
+ assert run is not None, path
File without changes
File without changes
File without changes
@@ -0,0 +1,21 @@
1
+ from unittest.mock import patch
2
+ import json
3
+ import os
4
+
5
+ from tests.utils import fixtures_path
6
+ from hestia_earth.orchestrator.models.emissions.deleted import run
7
+
8
+ folder_path = os.path.join(fixtures_path, 'orchestrator', 'emissions', 'deleted')
9
+
10
+
11
+ @patch('hestia_earth.orchestrator.strategies.merge._merge_version', return_value='0.0.0')
12
+ def test_run(*args):
13
+ with open(os.path.join(folder_path, 'config.json'), encoding='utf-8') as f:
14
+ config = json.load(f)
15
+ with open(os.path.join(folder_path, 'cycle.jsonld'), encoding='utf-8') as f:
16
+ cycle = json.load(f)
17
+ with open(os.path.join(folder_path, 'result.jsonld'), encoding='utf-8') as f:
18
+ expected = json.load(f)
19
+
20
+ result = run(config.get('models'), cycle)
21
+ assert result == expected
@@ -0,0 +1,29 @@
1
+ from unittest.mock import patch
2
+ import json
3
+ import os
4
+
5
+ from tests.utils import fixtures_path
6
+ from hestia_earth.orchestrator.models.transformations import run, _include_practice
7
+
8
+ folder_path = os.path.join(fixtures_path, 'orchestrator', 'transformation')
9
+
10
+
11
+ @patch('hestia_earth.orchestrator.strategies.merge._merge_version', return_value='0.0.0')
12
+ def test_run(*args):
13
+ with open(os.path.join(folder_path, 'config.json'), encoding='utf-8') as f:
14
+ config = json.load(f)
15
+ with open(os.path.join(folder_path, 'cycle.jsonld'), encoding='utf-8') as f:
16
+ cycle = json.load(f)
17
+ with open(os.path.join(folder_path, 'result.jsonld'), encoding='utf-8') as f:
18
+ expected = json.load(f)
19
+
20
+ result = run(config.get('models'), cycle)
21
+ assert result == expected
22
+
23
+
24
+ def test_include_practice():
25
+ term = {'@id': 'genericCropProduct', 'termType': 'crop'}
26
+ assert not _include_practice({'term': term})
27
+
28
+ term = {'@id': 'yieldOfPrimaryAquacultureProductLiveweightPerM2', 'termType': 'aquacultureManagement'}
29
+ assert _include_practice({'term': term}) is True
File without changes
File without changes
@@ -0,0 +1,33 @@
1
+ from unittest.mock import patch
2
+
3
+ from hestia_earth.orchestrator.strategies.merge.merge_append import merge
4
+
5
+ class_path = 'hestia_earth.orchestrator.strategies.merge.merge_append'
6
+
7
+
8
+ @patch(f"{class_path}.update_node_version", side_effect=lambda _v, n: n)
9
+ def test_merge_new_node(*args):
10
+ node1 = {
11
+ 'term': {'@id': 'term-1'},
12
+ 'value': 1
13
+ }
14
+ node2 = {
15
+ 'term': {'@id': 'term-2'},
16
+ 'value': 2
17
+ }
18
+ source = [node1]
19
+ result = merge(source, [node1, node2], '1')
20
+ result = merge(result, node2, '1')
21
+ assert result == [node1, node1, node2, node2]
22
+
23
+
24
+ def test_merge_list():
25
+ source = [1]
26
+ dest = [2, 3]
27
+ assert merge(source, dest, '1') == [1, 2, 3]
28
+
29
+
30
+ def test_merge_el():
31
+ source = [1]
32
+ dest = 2
33
+ assert merge(source, dest, '1') == [1, 2]
@@ -0,0 +1,7 @@
1
+ from hestia_earth.orchestrator.strategies.merge.merge_default import merge
2
+
3
+
4
+ def test_should_merge():
5
+ source = {'value': [100]}
6
+ dest = {'value': [50]}
7
+ assert merge(source, dest) == dest
@@ -0,0 +1,327 @@
1
+ from unittest.mock import patch
2
+ import pydash
3
+
4
+ from hestia_earth.orchestrator.strategies.merge.merge_list import merge
5
+
6
+ class_path = 'hestia_earth.orchestrator.strategies.merge.merge_list'
7
+ version = '1'
8
+
9
+
10
+ def _default_merge(a, b, *args): return pydash.objects.merge({}, a, b)
11
+
12
+
13
+ @patch(f"{class_path}.update_node_version", side_effect=lambda _v, n: n)
14
+ def test_merge_new_node(*args):
15
+ old_node = {
16
+ 'term': {'@id': 'old-term'},
17
+ 'value': 1
18
+ }
19
+ new_node = {
20
+ 'term': {'@id': 'new-term'},
21
+ 'value': 2
22
+ }
23
+ result = merge([old_node], [new_node], version)
24
+ assert result == [old_node, new_node]
25
+
26
+
27
+ @patch(f"{class_path}.merge_node", side_effect=_default_merge)
28
+ @patch(f"{class_path}.update_node_version", side_effect=lambda _v, n: n)
29
+ def test_merge_existing_node(*args):
30
+ term = {'@id': 'term'}
31
+
32
+ node_type = 'Site'
33
+ model = {'key': 'measurements'}
34
+
35
+ # with different value => should merge
36
+ old_node = {
37
+ 'term': term,
38
+ 'value': 1
39
+ }
40
+ new_node = {
41
+ 'term': term,
42
+ 'value': 2
43
+ }
44
+ result = merge([old_node], [new_node], version, model=model, node_type=node_type)
45
+ assert len(result) == 1
46
+
47
+ # with different depths => should not merge
48
+ result = merge([{
49
+ **old_node,
50
+ 'depthUpper': 100
51
+ }, {
52
+ **old_node,
53
+ 'depthUpper': 150
54
+ }], [{
55
+ **new_node,
56
+ 'depthUpper': 50
57
+ }], version, model=model, node_type=node_type)
58
+ assert len(result) == 3
59
+
60
+ node_type = 'Cycle'
61
+ model = {'key': 'emissions'}
62
+
63
+ # with same inputs => should merge
64
+ result = merge([{
65
+ **old_node,
66
+ 'inputs': [{'@id': 'input-1'}]
67
+ }], [{
68
+ **new_node,
69
+ 'inputs': [{'@id': 'input-1'}]
70
+ }], version, model=model, node_type=node_type)
71
+ assert len(result) == 1
72
+
73
+ # with different inputs => should not merge
74
+ result = merge([{
75
+ **old_node,
76
+ 'inputs': [{'@id': 'input-1'}]
77
+ }], [{
78
+ **new_node,
79
+ 'inputs': [{'@id': 'input-2'}]
80
+ }], version, model=model, node_type=node_type)
81
+ assert len(result) == 2
82
+
83
+ result = merge([{
84
+ **old_node,
85
+ 'inputs': [{'@id': 'input-1'}]
86
+ }], [{
87
+ **new_node,
88
+ 'inputs': [{'@id': 'input-1'}, {'@id': 'input-2'}]
89
+ }], version, model=model, node_type=node_type)
90
+ assert len(result) == 2
91
+
92
+ # with no inputs => should not merge
93
+ result = merge([old_node], [{
94
+ **new_node,
95
+ 'inputs': [{'@id': 'input-2'}]
96
+ }], version, model=model, node_type=node_type)
97
+ assert len(result) == 2
98
+
99
+
100
+ @patch(f"{class_path}.merge_node", side_effect=_default_merge)
101
+ @patch(f"{class_path}.update_node_version", side_effect=lambda _v, n: n)
102
+ def test_merge_existing_node_skip_same_term(*args):
103
+ term = {'@id': 'term'}
104
+ node_type = 'Site'
105
+ model = {'key': 'measurements'}
106
+
107
+ old_node = {
108
+ 'term': term,
109
+ 'value': 1
110
+ }
111
+ new_node = {
112
+ 'term': term,
113
+ 'value': 2
114
+ }
115
+ result = merge([old_node], [new_node], version, model, {'skipSameTerm': True}, node_type)
116
+ assert len(result) == 1
117
+ assert result[0]['value'] == 1
118
+
119
+
120
+ @patch(f"{class_path}.merge_node", side_effect=_default_merge)
121
+ @patch(f"{class_path}.update_node_version", side_effect=lambda _v, n: n)
122
+ def test_merge_existing_node_new_unique_key(*args):
123
+ term = {'@id': 'term'}
124
+ node_type = 'Cycle'
125
+ model = {'key': 'inputs'}
126
+
127
+ old_node = {
128
+ 'term': term,
129
+ 'value': 1
130
+ }
131
+ new_node = {
132
+ 'term': term,
133
+ 'value': 1,
134
+ 'impactAssessment': {'@id': 'ia-1'}
135
+ }
136
+ result = merge([old_node], [new_node], version, model, {}, node_type)
137
+ assert len(result) == 1
138
+ assert result[0]['impactAssessment'] == {'@id': 'ia-1'}
139
+
140
+
141
+ @patch(f"{class_path}.merge_node", side_effect=_default_merge)
142
+ @patch(f"{class_path}.update_node_version", side_effect=lambda _v, n: n)
143
+ def test_merge_authors(*args):
144
+ node_type = 'Bibliography'
145
+ model = {'key': 'authors'}
146
+
147
+ old_node = {
148
+ 'lastName': 'name 1'
149
+ }
150
+ new_node = {
151
+ 'lastName': 'name 2'
152
+ }
153
+ result = merge([old_node], [new_node], version, model, {}, node_type)
154
+ # no unique keys, should just append data
155
+ assert result == [old_node, new_node]
156
+
157
+
158
+ @patch(f"{class_path}.merge_node", side_effect=_default_merge)
159
+ @patch(f"{class_path}.update_node_version", side_effect=lambda _v, n: n)
160
+ def test_merge_different_terms_same_unique_properties(*args):
161
+ method = {'@id': 'method1'}
162
+ operation = {'@id': 'operation1'}
163
+ inputs = [{'@id': 'input1'}, {'@id': 'input2'}]
164
+ node_type = 'ImpactAssessment'
165
+ model = {'key': 'emissionsResourceUse'}
166
+
167
+ node1 = {
168
+ 'term': {'@id': 'term1'},
169
+ 'value': 1,
170
+ 'methodModel': method,
171
+ 'operation': operation,
172
+ 'inputs': inputs
173
+ }
174
+ node2 = {
175
+ 'term': {'@id': 'term2'},
176
+ 'value': 2,
177
+ 'methodModel': method,
178
+ 'operation': operation,
179
+ 'inputs': inputs
180
+ }
181
+ node3 = {
182
+ 'term': {'@id': 'term3'},
183
+ 'value': 3,
184
+ 'inputs': inputs
185
+ }
186
+ node4 = {
187
+ 'term': {'@id': 'term1'},
188
+ 'value': 2,
189
+ 'methodModel': method,
190
+ 'operation': operation,
191
+ 'inputs': inputs
192
+ }
193
+ # different term should not merge
194
+ assert merge([node1], [node2], version, model, {'sameMethodModel': True}, node_type) == [node1, node2]
195
+ assert merge([node1, node2], [node3], version, model, {'sameMethodModel': True}, node_type) == [node1, node2, node3]
196
+ assert merge([node1], [node2, node3], version, model, {}, node_type) == [node1, node2, node3]
197
+ # same term, methodModel, operation and inputs should merge
198
+ assert merge([node1], [node4], version, model, {}, node_type) == [node4]
199
+
200
+
201
+ @patch(f"{class_path}.merge_node", side_effect=_default_merge)
202
+ @patch(f"{class_path}.update_node_version", side_effect=lambda _v, n: n)
203
+ def test_merge_multiple_identical_terms(*args):
204
+ term1 = {'@id': 'term1'}
205
+ node_type = 'Cycle'
206
+ model = {'key': 'inputs'}
207
+
208
+ node1 = {
209
+ 'term': term1,
210
+ 'value': 1
211
+ }
212
+ node2 = {
213
+ 'term': term1,
214
+ 'value': 2,
215
+ 'impactAssessment': {'id': 'ia-1'}
216
+ }
217
+ # merging the same unique nodes should make no changes
218
+ result = merge([node1, node2], [node1, node2], version, model, {}, node_type)
219
+ assert result == [node1, node2]
220
+
221
+ # adding the same first node with a new unique key
222
+ node3 = {
223
+ 'term': term1,
224
+ 'value': 3,
225
+ 'impactAssessment': {'@id': 'ia-1'}
226
+ }
227
+ result = merge([node1, node2], [node3], version, model, {}, node_type)
228
+ assert result == [node3, node2]
229
+
230
+ node4 = {
231
+ 'term': term1,
232
+ 'value': 4,
233
+ 'impactAssessment': {'id': 'ia-1'}
234
+ }
235
+ result = merge([node1, node2], [node4], version, model, {}, node_type)
236
+ assert result == [node1, node4]
237
+
238
+
239
+ @patch(f"{class_path}.merge_node", side_effect=_default_merge)
240
+ @patch(f"{class_path}.update_node_version", side_effect=lambda _v, n: n)
241
+ def test_merge_animals(*args):
242
+ term1 = {'@id': 'term1'}
243
+ node_type = 'Cycle'
244
+ model = {'key': 'animals'}
245
+
246
+ node1 = {
247
+ 'animalId': 'animal-1',
248
+ 'term': term1,
249
+ 'value': 1,
250
+ 'properties': [
251
+ {
252
+ 'term': {"@id": "liveweightPerHead"},
253
+ 'value': 40
254
+ }
255
+ ]
256
+ }
257
+ node2 = {
258
+ 'animalId': 'animal-2',
259
+ 'term': term1,
260
+ 'value': 1,
261
+ 'properties': [
262
+ {
263
+ 'term': {"@id": "liveweightPerHead"},
264
+ 'value': 40
265
+ },
266
+ {
267
+ 'term': {"@id": "age"},
268
+ 'value': 10
269
+ }
270
+ ]
271
+ }
272
+ result = merge([node1], [node2], version, model, {}, node_type)
273
+ # can not merge as properties is used to determine uniqueness
274
+ assert result == [node1, node2]
275
+
276
+
277
+ @patch(f"{class_path}.merge_node", side_effect=_default_merge)
278
+ @patch(f"{class_path}.update_node_version", side_effect=lambda _v, n: n)
279
+ def test_merge_with_properties(*args):
280
+ node_type = 'Cycle'
281
+ model = {'key': 'inputs'}
282
+
283
+ node1 = {
284
+ "term": {
285
+ "@type": "Term",
286
+ "@id": "concentrateFeedBlend"
287
+ },
288
+ "isAnimalFeed": True,
289
+ "value": [
290
+ 100
291
+ ],
292
+ "impactAssessment": {
293
+ "@id": "_djxbkdk2wnx",
294
+ "@type": "ImpactAssessment"
295
+ },
296
+ "impactAssessmentIsProxy": False,
297
+ "@type": "Input"
298
+ }
299
+
300
+ node2 = {
301
+ "term": {
302
+ "@type": "Term",
303
+ "@id": "concentrateFeedBlend"
304
+ },
305
+ "isAnimalFeed": True,
306
+ "value": [
307
+ 200
308
+ ],
309
+ "impactAssessment": {
310
+ "@id": "uug7pcaas6aa",
311
+ "@type": "ImpactAssessment"
312
+ },
313
+ "impactAssessmentIsProxy": False,
314
+ "@type": "Input"
315
+ }
316
+
317
+ properties = [
318
+ {
319
+ 'term': {'@id': 'property-1'},
320
+ 'value': 100
321
+ }
322
+ ]
323
+ node3 = node2 | {'properties': properties}
324
+
325
+ result = merge([node1, node2], [node3], version, model, {}, node_type)
326
+ # can not merge as properties is used to determine uniqueness
327
+ assert result == [node1, node3]
@@ -0,0 +1,95 @@
1
+ from unittest.mock import patch
2
+ from hestia_earth.schema import EmissionMethodTier
3
+
4
+ from hestia_earth.orchestrator.strategies.merge.merge_node import merge, _has_threshold_diff, _should_merge_lower_tier
5
+
6
+ class_path = 'hestia_earth.orchestrator.strategies.merge.merge_node'
7
+
8
+
9
+ def test_has_threshold_diff():
10
+ key = 'key'
11
+ threshold = 0.1 # 10%
12
+ source = {}
13
+ dest = {}
14
+
15
+ # key not in source => merge
16
+ assert _has_threshold_diff(source, dest, key, threshold) is True
17
+
18
+ # key not in dest => no merge
19
+ source[key] = [100]
20
+ assert not _has_threshold_diff(source, dest, key, threshold)
21
+
22
+ dest[key] = [90]
23
+ assert not _has_threshold_diff(source, dest, key, threshold)
24
+
25
+ dest[key] = [89]
26
+ assert _has_threshold_diff(source, dest, key, threshold) is True
27
+
28
+ # edge cases
29
+ source[key] = [0]
30
+ dest[key] = [1]
31
+ assert _has_threshold_diff(source, dest, key, threshold) is True
32
+
33
+ source[key] = [0]
34
+ dest[key] = [0]
35
+ assert not _has_threshold_diff(source, dest, key, threshold)
36
+
37
+ source[key] = [1]
38
+ dest[key] = [0]
39
+ assert _has_threshold_diff(source, dest, key, threshold)
40
+
41
+
42
+ def test_should_merge_lower_tier():
43
+ source = {}
44
+ dest = {}
45
+
46
+ # always merge if replacing lower tier
47
+ assert _should_merge_lower_tier(source, dest, {'replaceLowerTier': True}) is True
48
+
49
+ # new value has lower tier
50
+ source['methodTier'] = EmissionMethodTier.TIER_3.value
51
+ dest['methodTier'] = EmissionMethodTier.TIER_1.value
52
+ assert not _should_merge_lower_tier(source, dest, {'replaceLowerTier': False})
53
+
54
+ source['methodTier'] = EmissionMethodTier.TIER_1.value
55
+ dest['methodTier'] = EmissionMethodTier.NOT_RELEVANT.value
56
+ assert not _should_merge_lower_tier(source, dest, {'replaceLowerTier': False})
57
+
58
+ # new value has identical tier
59
+ source['methodTier'] = EmissionMethodTier.TIER_1.value
60
+ dest['methodTier'] = EmissionMethodTier.TIER_1.value
61
+ assert _should_merge_lower_tier(source, dest, {'replaceLowerTier': False}) is True
62
+
63
+ # new value has higher tier
64
+ source['methodTier'] = EmissionMethodTier.TIER_1.value
65
+ dest['methodTier'] = EmissionMethodTier.TIER_3.value
66
+ assert _should_merge_lower_tier(source, dest, {'replaceLowerTier': False}) is True
67
+
68
+
69
+ @patch(f"{class_path}._has_threshold_diff", return_value=False)
70
+ def test_merge_no_merge(*args):
71
+ source = {'value': [100]}
72
+ dest = {'value': [50]}
73
+ args = {'replaceThreshold': ['value', 50]}
74
+ # simply return the source
75
+ assert merge(source, dest, '0', {}, args) == source
76
+
77
+
78
+ @patch(f"{class_path}._has_threshold_diff", return_value=True)
79
+ @patch(f"{class_path}.update_node_version", return_value={})
80
+ def test_merge_no_threshold(mock_update, *args):
81
+ source = {'value': [100]}
82
+ dest = {'value': [50]}
83
+ args = {}
84
+ merge(source, dest, '0', {}, args)
85
+ mock_update.assert_called_once()
86
+
87
+
88
+ @patch(f"{class_path}._has_threshold_diff", return_value=True)
89
+ @patch(f"{class_path}.update_node_version", return_value={})
90
+ def test_merge_with_threshold(mock_update, *args):
91
+ source = {'value': [100]}
92
+ dest = {'value': [50]}
93
+ args = {'replaceThreshold': ['value', 50]}
94
+ merge(source, dest, '0', {}, args)
95
+ mock_update.assert_called_once()
File without changes