fmu-sumo 2.3.6__py3-none-any.whl → 2.3.8__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.
@@ -1,19 +1,27 @@
1
+ from __future__ import annotations
2
+
1
3
  import uuid
2
4
  import warnings
3
5
  from datetime import datetime
4
6
  from io import BytesIO
5
- from typing import Dict, List, Tuple
7
+ from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Union
6
8
 
7
9
  import deprecation
8
10
  import httpx
9
- from sumo.wrapper import SumoClient
10
11
 
11
- import fmu.sumo.explorer.objects as objects
12
+ from fmu.sumo.explorer import objects
12
13
  from fmu.sumo.explorer.cache import LRUCache
13
14
 
15
+ if TYPE_CHECKING:
16
+ from sumo.wrapper import SumoClient
17
+
18
+
19
+ # Type aliases
20
+ SelectArg = Union[bool, str, Dict[str, Union[str, List[str]]], List[str]]
21
+
14
22
 
15
23
  def _gen_filter_none():
16
- def _fn(value):
24
+ def _fn(_):
17
25
  return None, None
18
26
 
19
27
  return _fn
@@ -211,7 +219,7 @@ class Pit:
211
219
  self._id = res.json()["id"]
212
220
  return self
213
221
 
214
- def __exit__(self, exc_type, exc_value, traceback):
222
+ def __exit__(self, *_):
215
223
  if self._id is not None:
216
224
  self._sumo.delete("/pit", params={"id": self._id})
217
225
  pass
@@ -224,7 +232,7 @@ class Pit:
224
232
  self._id = res.json()["id"]
225
233
  return self
226
234
 
227
- async def __aexit__(self, exc_type, exc_value, traceback):
235
+ async def __aexit__(self, *_):
228
236
  if self._id is not None:
229
237
  await self._sumo.delete_async("/pit", params={"id": self._id})
230
238
  pass
@@ -257,7 +265,7 @@ class SearchContext:
257
265
  self._hits = None
258
266
  self._cache = LRUCache(capacity=200)
259
267
  self._length = None
260
- self._select = {
268
+ self._select: SelectArg = {
261
269
  "excludes": ["fmu.realization.parameters"],
262
270
  }
263
271
  return
@@ -282,7 +290,7 @@ class SearchContext:
282
290
  else:
283
291
  return {"bool": {"must": must, "must_not": must_not}}
284
292
 
285
- def _to_sumo(self, obj, blob=None):
293
+ def _to_sumo(self, obj, blob=None) -> objects.Document:
286
294
  cls = obj["_source"]["class"]
287
295
  if cls == "case":
288
296
  return objects.Case(self._sumo, obj)
@@ -295,6 +303,8 @@ class SearchContext:
295
303
  "table": objects.Table,
296
304
  "cpgrid": objects.CPGrid,
297
305
  "cpgrid_property": objects.CPGridProperty,
306
+ "iteration": objects.Iteration,
307
+ "realization": objects.Realization,
298
308
  }.get(cls)
299
309
  if constructor is None:
300
310
  warnings.warn(f"No constructor for class {cls}")
@@ -319,7 +329,7 @@ class SearchContext:
319
329
  self._length = res["count"]
320
330
  return self._length
321
331
 
322
- def __search_all(self, query, size=1000, select=False):
332
+ def __search_all(self, query, size: int = 1000, select: SelectArg = False):
323
333
  all_hits = []
324
334
  query = {
325
335
  "query": query,
@@ -356,10 +366,12 @@ class SearchContext:
356
366
  pass
357
367
  return all_hits
358
368
 
359
- def _search_all(self, select=False):
369
+ def _search_all(self, select: SelectArg = False):
360
370
  return self.__search_all(query=self._query, size=1000, select=select)
361
371
 
362
- async def __search_all_async(self, query, size=1000, select=False):
372
+ async def __search_all_async(
373
+ self, query, size: int = 1000, select: SelectArg = False
374
+ ):
363
375
  all_hits = []
364
376
  query = {
365
377
  "query": query,
@@ -398,7 +410,7 @@ class SearchContext:
398
410
  pass
399
411
  return all_hits
400
412
 
401
- async def _search_all_async(self, select=False):
413
+ async def _search_all_async(self, select: SelectArg = False):
402
414
  return await self.__search_all_async(
403
415
  query=self._query, size=1000, select=select
404
416
  )
@@ -477,7 +489,7 @@ class SearchContext:
477
489
  assert await self.length_async() == 1
478
490
  return await self.getitem_async(0)
479
491
 
480
- def select(self, sel):
492
+ def select(self, sel) -> SearchContext:
481
493
  """Specify what should be returned from elasticsearch.
482
494
  Has the side effect of clearing the lru cache.
483
495
  sel is either a single string value, a list of string value,
@@ -521,7 +533,7 @@ class SearchContext:
521
533
  self._cache.clear()
522
534
  return self
523
535
 
524
- def get_object(self, uuid: str) -> Dict:
536
+ def get_object(self, uuid: str) -> objects.Document:
525
537
  """Get metadata object by uuid
526
538
 
527
539
  Args:
@@ -549,7 +561,7 @@ class SearchContext:
549
561
 
550
562
  return self._to_sumo(obj)
551
563
 
552
- async def get_object_async(self, uuid: str) -> Dict:
564
+ async def get_object_async(self, uuid: str) -> objects.Document:
553
565
  """Get metadata object by uuid
554
566
 
555
567
  Args:
@@ -579,6 +591,7 @@ class SearchContext:
579
591
  return self._to_sumo(obj)
580
592
 
581
593
  def _maybe_prefetch(self, index):
594
+ assert isinstance(self._hits, list)
582
595
  uuid = self._hits[index]
583
596
  if self._cache.has(uuid):
584
597
  return
@@ -595,6 +608,7 @@ class SearchContext:
595
608
  return
596
609
 
597
610
  async def _maybe_prefetch_async(self, index):
611
+ assert isinstance(self._hits, list)
598
612
  uuid = self._hits[index]
599
613
  if self._cache.has(uuid):
600
614
  return
@@ -613,13 +627,13 @@ class SearchContext:
613
627
  def get_objects(
614
628
  self,
615
629
  uuids: List[str],
616
- select: List[str] = None,
630
+ select: SelectArg,
617
631
  ) -> List[Dict]:
618
632
  size = (
619
633
  1000
620
634
  if select is False
621
635
  else 100
622
- if isinstance(select, list)
636
+ if isinstance(select, (list, dict))
623
637
  else 10
624
638
  )
625
639
  return self.__search_all(
@@ -627,15 +641,13 @@ class SearchContext:
627
641
  )
628
642
 
629
643
  async def get_objects_async(
630
- self,
631
- uuids: List[str],
632
- select: List[str] = None,
644
+ self, uuids: List[str], select: SelectArg
633
645
  ) -> List[Dict]:
634
646
  size = (
635
647
  1000
636
648
  if select is False
637
649
  else 100
638
- if isinstance(select, list)
650
+ if isinstance(select, (list, dict))
639
651
  else 10
640
652
  )
641
653
  return await self.__search_all_async(
@@ -900,8 +912,8 @@ class SearchContext:
900
912
  return objects.Realizations(self, uuids)
901
913
 
902
914
  @property
903
- def template_paths(search_context): # noqa: N805
904
- return {obj.template_path for obj in search_context}
915
+ def template_paths(self) -> List[str]:
916
+ return {obj.template_path for obj in self}
905
917
 
906
918
  @property
907
919
  def metrics(self):
@@ -919,9 +931,9 @@ class SearchContext:
919
931
  @property
920
932
  async def timestamps_async(self) -> List[str]:
921
933
  """List of unique timestamps in SearchContext"""
922
- ts = await self.get_field_values_async(
923
- "data.time.t0.value", self._timestamp_query
924
- )
934
+ ts = await self.filter(
935
+ complex=self._timestamp_query
936
+ ).get_field_values_async("data.time.t0.value")
925
937
  return [datetime.fromtimestamp(t / 1000).isoformat() for t in ts]
926
938
 
927
939
  def _extract_intervals(self, res):
@@ -987,7 +999,6 @@ class SearchContext:
987
999
  f = filters.get(k)
988
1000
  if f is None:
989
1001
  raise Exception(f"Don't know how to generate filter for {k}")
990
- pass
991
1002
  _must, _must_not = f(v)
992
1003
  if _must:
993
1004
  must.append(_must)
@@ -1023,46 +1034,46 @@ class SearchContext:
1023
1034
  return sc
1024
1035
 
1025
1036
  @property
1026
- def surfaces(self):
1037
+ def surfaces(self) -> SearchContext:
1027
1038
  return self._context_for_class("surface")
1028
1039
 
1029
1040
  @property
1030
- def tables(self):
1041
+ def tables(self) -> SearchContext:
1031
1042
  return self._context_for_class("table")
1032
1043
 
1033
1044
  @property
1034
- def cubes(self):
1045
+ def cubes(self) -> SearchContext:
1035
1046
  return self._context_for_class("cube")
1036
1047
 
1037
1048
  @property
1038
- def polygons(self):
1049
+ def polygons(self) -> SearchContext:
1039
1050
  return self._context_for_class("polygons")
1040
1051
 
1041
1052
  @property
1042
- def dictionaries(self):
1053
+ def dictionaries(self) -> SearchContext:
1043
1054
  return self._context_for_class("dictionary")
1044
1055
 
1045
1056
  @property
1046
- def grids(self):
1057
+ def grids(self) -> SearchContext:
1047
1058
  return self._context_for_class("cpgrid")
1048
1059
 
1049
1060
  @property
1050
- def grid_properties(self):
1061
+ def grid_properties(self) -> SearchContext:
1051
1062
  return self._context_for_class("cpgrid_property")
1052
1063
 
1053
- def _get_object_by_class_and_uuid(self, cls, uuid):
1064
+ def _get_object_by_class_and_uuid(self, cls, uuid) -> Any:
1054
1065
  obj = self.get_object(uuid)
1055
1066
  if obj.metadata["class"] != cls:
1056
1067
  raise Exception(f"Document of type {cls} not found: {uuid}")
1057
1068
  return obj
1058
1069
 
1059
- async def _get_object_by_class_and_uuid_async(self, cls, uuid):
1070
+ async def _get_object_by_class_and_uuid_async(self, cls, uuid) -> Any:
1060
1071
  obj = await self.get_object_async(uuid)
1061
1072
  if obj.metadata["class"] != cls:
1062
1073
  raise Exception(f"Document of type {cls} not found: {uuid}")
1063
1074
  return obj
1064
1075
 
1065
- def get_case_by_uuid(self, uuid: str):
1076
+ def get_case_by_uuid(self, uuid: str) -> objects.Case:
1066
1077
  """Get case object by uuid
1067
1078
 
1068
1079
  Args:
@@ -1073,7 +1084,7 @@ class SearchContext:
1073
1084
  """
1074
1085
  return self._get_object_by_class_and_uuid("case", uuid)
1075
1086
 
1076
- async def get_case_by_uuid_async(self, uuid: str):
1087
+ async def get_case_by_uuid_async(self, uuid: str) -> objects.Case:
1077
1088
  """Get case object by uuid
1078
1089
 
1079
1090
  Args:
@@ -1091,6 +1102,7 @@ class SearchContext:
1091
1102
  "_source": {
1092
1103
  "includes": [
1093
1104
  "$schema",
1105
+ "class",
1094
1106
  "source",
1095
1107
  "version",
1096
1108
  "access",
@@ -1101,7 +1113,7 @@ class SearchContext:
1101
1113
  },
1102
1114
  }
1103
1115
 
1104
- def get_iteration_by_uuid(self, uuid: str):
1116
+ def get_iteration_by_uuid(self, uuid: str) -> objects.Iteration:
1105
1117
  """Get iteration object by uuid
1106
1118
 
1107
1119
  Args:
@@ -1109,14 +1121,25 @@ class SearchContext:
1109
1121
 
1110
1122
  Returns: iteration object
1111
1123
  """
1112
- res = self._sumo.post(
1113
- "/search", json=self._iteration_query(uuid)
1114
- ).json()
1115
- obj = res["hits"]["hits"][0]
1116
- obj["_id"] = uuid
1117
- return objects.Iteration(self._sumo, obj)
1124
+ try:
1125
+ obj = self.get_object(uuid)
1126
+ assert isinstance(obj, objects.Iteration)
1127
+ return obj
1128
+ except Exception:
1129
+ res = self._sumo.post(
1130
+ "/search", json=self._iteration_query(uuid)
1131
+ ).json()
1132
+ hits = res["hits"]["hits"]
1133
+ if len(hits) == 0:
1134
+ raise Exception(f"Document not found: {uuid}")
1135
+ obj = hits[0]
1136
+ obj["_id"] = uuid
1137
+ obj["_source"]["class"] = "iteration"
1138
+ return self._to_sumo(obj)
1118
1139
 
1119
- async def get_iteration_by_uuid_async(self, uuid: str):
1140
+ async def get_iteration_by_uuid_async(
1141
+ self, uuid: str
1142
+ ) -> objects.Iteration:
1120
1143
  """Get iteration object by uuid
1121
1144
 
1122
1145
  Args:
@@ -1124,16 +1147,25 @@ class SearchContext:
1124
1147
 
1125
1148
  Returns: iteration object
1126
1149
  """
1127
- res = (
1128
- await self._sumo.post_async(
1129
- "/search", json=self._iteration_query(uuid)
1130
- )
1131
- ).json()
1132
- obj = res["hits"]["hits"][0]
1133
- obj["_id"] = uuid
1134
- return objects.Iteration(self._sumo, obj)
1150
+ try:
1151
+ obj = await self.get_object_async(uuid)
1152
+ assert isinstance(obj, objects.Iteration)
1153
+ return obj
1154
+ except Exception:
1155
+ res = (
1156
+ await self._sumo.post_async(
1157
+ "/search", json=self._iteration_query(uuid)
1158
+ )
1159
+ ).json()
1160
+ hits = res["hits"]["hits"]
1161
+ if len(hits) == 0:
1162
+ raise Exception(f"Document not found: {uuid}")
1163
+ obj = hits[0]
1164
+ obj["_id"] = uuid
1165
+ obj["_source"]["class"] = "iteration"
1166
+ return self._to_sumo(obj)
1135
1167
 
1136
- def _realization_query(self, uuid):
1168
+ def _realization_query(self, uuid) -> Dict:
1137
1169
  return {
1138
1170
  "query": {
1139
1171
  "term": {"fmu.realization.uuid.keyword": {"value": uuid}}
@@ -1142,6 +1174,7 @@ class SearchContext:
1142
1174
  "_source": {
1143
1175
  "includes": [
1144
1176
  "$schema",
1177
+ "class",
1145
1178
  "source",
1146
1179
  "version",
1147
1180
  "access",
@@ -1153,7 +1186,7 @@ class SearchContext:
1153
1186
  },
1154
1187
  }
1155
1188
 
1156
- def get_realization_by_uuid(self, uuid: str):
1189
+ def get_realization_by_uuid(self, uuid: str) -> objects.Realization:
1157
1190
  """Get realization object by uuid
1158
1191
 
1159
1192
  Args:
@@ -1161,14 +1194,25 @@ class SearchContext:
1161
1194
 
1162
1195
  Returns: realization object
1163
1196
  """
1164
- res = self._sumo.post(
1165
- "/search", json=self._realization_query(uuid)
1166
- ).json()
1167
- obj = res["hits"]["hits"][0]
1168
- obj["_id"] = uuid
1169
- return objects.Realization(self._sumo, obj)
1197
+ try:
1198
+ obj = self.get_object(uuid)
1199
+ assert isinstance(obj, objects.Realization)
1200
+ return obj
1201
+ except Exception:
1202
+ res = self._sumo.post(
1203
+ "/search", json=self._realization_query(uuid)
1204
+ ).json()
1205
+ hits = res["hits"]["hits"]
1206
+ if len(hits) == 0:
1207
+ raise Exception(f"Document not found: {uuid}")
1208
+ obj = hits[0]
1209
+ obj["_id"] = uuid
1210
+ obj["_source"]["class"] = "realization"
1211
+ return self._to_sumo(obj)
1170
1212
 
1171
- async def get_realization_by_uuid_async(self, uuid: str):
1213
+ async def get_realization_by_uuid_async(
1214
+ self, uuid: str
1215
+ ) -> objects.Realization:
1172
1216
  """Get realization object by uuid
1173
1217
 
1174
1218
  Args:
@@ -1176,16 +1220,25 @@ class SearchContext:
1176
1220
 
1177
1221
  Returns: realization object
1178
1222
  """
1179
- res = (
1180
- await self._sumo.post(
1181
- "/search", json=self._realization_query(uuid)
1182
- )
1183
- ).json()
1184
- obj = res["hits"]["hits"][0]
1185
- obj["_id"] = uuid
1186
- return objects.Realization(self._sumo, obj)
1223
+ try:
1224
+ obj = await self.get_object_async(uuid)
1225
+ assert isinstance(obj, objects.Realization)
1226
+ return obj
1227
+ except Exception:
1228
+ res = (
1229
+ await self._sumo.post_async(
1230
+ "/search", json=self._realization_query(uuid)
1231
+ )
1232
+ ).json()
1233
+ hits = res["hits"]["hits"]
1234
+ if len(hits) == 0:
1235
+ raise Exception(f"Document not found: {uuid}")
1236
+ obj = hits[0]
1237
+ obj["_id"] = uuid
1238
+ obj["_source"]["class"] = "realization"
1239
+ return self._to_sumo(obj)
1187
1240
 
1188
- def get_surface_by_uuid(self, uuid: str):
1241
+ def get_surface_by_uuid(self, uuid: str) -> objects.Surface:
1189
1242
  """Get surface object by uuid
1190
1243
 
1191
1244
  Args:
@@ -1196,7 +1249,7 @@ class SearchContext:
1196
1249
  """
1197
1250
  return self._get_object_by_class_and_uuid("surface", uuid)
1198
1251
 
1199
- async def get_surface_by_uuid_async(self, uuid: str):
1252
+ async def get_surface_by_uuid_async(self, uuid: str) -> objects.Surface:
1200
1253
  """Get surface object by uuid
1201
1254
 
1202
1255
  Args:
@@ -1207,7 +1260,7 @@ class SearchContext:
1207
1260
  """
1208
1261
  return await self._get_object_by_class_and_uuid_async("surface", uuid)
1209
1262
 
1210
- def get_polygons_by_uuid(self, uuid: str):
1263
+ def get_polygons_by_uuid(self, uuid: str) -> objects.Polygons:
1211
1264
  """Get polygons object by uuid
1212
1265
 
1213
1266
  Args:
@@ -1218,7 +1271,7 @@ class SearchContext:
1218
1271
  """
1219
1272
  return self._get_object_by_class_and_uuid("polygons", uuid)
1220
1273
 
1221
- async def get_polygons_by_uuid_async(self, uuid: str):
1274
+ async def get_polygons_by_uuid_async(self, uuid: str) -> objects.Polygons:
1222
1275
  """Get polygons object by uuid
1223
1276
 
1224
1277
  Args:
@@ -1229,7 +1282,7 @@ class SearchContext:
1229
1282
  """
1230
1283
  return await self._get_object_by_class_and_uuid_async("polygons", uuid)
1231
1284
 
1232
- def get_table_by_uuid(self, uuid: str):
1285
+ def get_table_by_uuid(self, uuid: str) -> objects.Table:
1233
1286
  """Get table object by uuid
1234
1287
 
1235
1288
  Args:
@@ -1240,7 +1293,7 @@ class SearchContext:
1240
1293
  """
1241
1294
  return self._get_object_by_class_and_uuid("table", uuid)
1242
1295
 
1243
- async def get_table_by_uuid_async(self, uuid: str):
1296
+ async def get_table_by_uuid_async(self, uuid: str) -> objects.Table:
1244
1297
  """Get table object by uuid
1245
1298
 
1246
1299
  Args:
@@ -1251,7 +1304,9 @@ class SearchContext:
1251
1304
  """
1252
1305
  return await self._get_object_by_class_and_uuid_async("table", uuid)
1253
1306
 
1254
- def _verify_aggregation_operation(self):
1307
+ def _verify_aggregation_operation(
1308
+ self,
1309
+ ) -> Tuple[Dict, List[str], List[int]]:
1255
1310
  query = {
1256
1311
  "query": self._query,
1257
1312
  "size": 1,
@@ -1269,6 +1324,8 @@ class SearchContext:
1269
1324
  },
1270
1325
  }
1271
1326
  sres = self._sumo.post("/search", json=query).json()
1327
+ if len(sres["hits"]["hits"]) == 0:
1328
+ raise Exception("No matching realizations found.")
1272
1329
  prototype = sres["hits"]["hits"][0]
1273
1330
  conflicts = [
1274
1331
  k
@@ -1291,8 +1348,15 @@ class SearchContext:
1291
1348
  rids = [hit["_source"]["fmu"]["realization"]["id"] for hit in hits]
1292
1349
  return prototype, uuids, rids
1293
1350
 
1294
- def _aggregate(self, columns=None, operation=None):
1295
- prototype, uuids, rids = self._verify_aggregation_operation()
1351
+ def _aggregate(self, columns=None, operation=None) -> objects.Child:
1352
+ assert (
1353
+ operation != "collection"
1354
+ or columns is not None
1355
+ and len(columns) == 1
1356
+ ), "Exactly one column required for collection aggregation."
1357
+ prototype, uuids, rids = self.filter(
1358
+ column=columns
1359
+ )._verify_aggregation_operation()
1296
1360
  spec = {
1297
1361
  "object_ids": uuids,
1298
1362
  "operations": [operation],
@@ -1330,10 +1394,11 @@ class SearchContext:
1330
1394
  raise ex
1331
1395
  blob = BytesIO(res.content)
1332
1396
  res = self._to_sumo(prototype, blob)
1397
+ assert isinstance(res, objects.Child)
1333
1398
  res._blob = blob
1334
1399
  return res
1335
1400
 
1336
- def aggregate(self, columns=None, operation=None):
1401
+ def aggregate(self, columns=None, operation=None) -> objects.Child:
1337
1402
  if len(self.hidden) > 0:
1338
1403
  return self.hidden._aggregate(columns=columns, operation=operation)
1339
1404
  else:
@@ -1341,7 +1406,9 @@ class SearchContext:
1341
1406
  columns=columns, operation=operation
1342
1407
  )
1343
1408
 
1344
- async def _verify_aggregation_operation_async(self):
1409
+ async def _verify_aggregation_operation_async(
1410
+ self,
1411
+ ) -> Tuple[Dict, List[str], List[int]]:
1345
1412
  query = {
1346
1413
  "query": self._query,
1347
1414
  "size": 1,
@@ -1359,6 +1426,8 @@ class SearchContext:
1359
1426
  },
1360
1427
  }
1361
1428
  sres = (await self._sumo.post_async("/search", json=query)).json()
1429
+ if len(sres["hits"]["hits"]) == 0:
1430
+ raise Exception("No matching realizations found.")
1362
1431
  prototype = sres["hits"]["hits"][0]
1363
1432
  conflicts = [
1364
1433
  k
@@ -1381,12 +1450,21 @@ class SearchContext:
1381
1450
  rids = [hit["_source"]["fmu"]["realization"]["id"] for hit in hits]
1382
1451
  return prototype, uuids, rids
1383
1452
 
1384
- async def _aggregate_async(self, columns=None, operation=None):
1453
+ async def _aggregate_async(
1454
+ self, columns=None, operation=None
1455
+ ) -> objects.Child:
1456
+ assert (
1457
+ operation != "collection"
1458
+ or columns is not None
1459
+ and len(columns) == 1
1460
+ ), "Exactly one column required for collection aggregation."
1385
1461
  (
1386
1462
  prototype,
1387
1463
  uuids,
1388
1464
  rids,
1389
- ) = await self._verify_aggregation_operation_async()
1465
+ ) = await self.filter(
1466
+ column=columns
1467
+ )._verify_aggregation_operation_async()
1390
1468
  spec = {
1391
1469
  "object_ids": uuids,
1392
1470
  "operations": [operation],
@@ -1424,10 +1502,13 @@ class SearchContext:
1424
1502
  raise ex
1425
1503
  blob = BytesIO(res.content)
1426
1504
  res = self._to_sumo(prototype, blob)
1505
+ assert isinstance(res, objects.Child)
1427
1506
  res._blob = blob
1428
1507
  return res
1429
1508
 
1430
- async def aggregate_async(self, columns=None, operation=None):
1509
+ async def aggregate_async(
1510
+ self, columns=None, operation=None
1511
+ ) -> objects.Child:
1431
1512
  length_hidden = await self.hidden.length_async()
1432
1513
  if length_hidden > 0:
1433
1514
  return await self.hidden._aggregate_async(
@@ -1438,33 +1519,55 @@ class SearchContext:
1438
1519
  columns=columns, operation=operation
1439
1520
  )
1440
1521
 
1441
- def aggregation(self, column=None, operation=None):
1522
+ def aggregation(self, column=None, operation=None) -> objects.Child:
1442
1523
  assert operation is not None
1443
1524
  assert column is None or isinstance(column, str)
1444
1525
  sc = self.filter(aggregation=operation, column=column)
1445
1526
  numaggs = len(sc)
1446
1527
  assert numaggs <= 1
1447
1528
  if numaggs == 1:
1448
- return sc[0]
1449
- else:
1450
- return self.filter(realization=True).aggregate(
1451
- columns=[column] if column is not None else None,
1452
- operation=operation,
1453
- )
1529
+ agg = sc.single
1530
+ assert isinstance(agg, objects.Child)
1531
+ ts = agg.metadata["_sumo"]["timestamp"]
1532
+ reals = self.filter(
1533
+ realization=True,
1534
+ complex={"range": {"_sumo.timestamp": {"lt": ts}}},
1535
+ ).realizationids
1536
+ if set(reals) == set(
1537
+ agg.metadata["fmu"]["aggregation"]["realization_ids"]
1538
+ ):
1539
+ return agg
1540
+ # ELSE
1541
+ return self.filter(realization=True).aggregate(
1542
+ columns=[column] if column is not None else None,
1543
+ operation=operation,
1544
+ )
1454
1545
 
1455
- async def aggregation_async(self, column=None, operation=None):
1546
+ async def aggregation_async(
1547
+ self, column=None, operation=None
1548
+ ) -> objects.Child:
1456
1549
  assert operation is not None
1457
1550
  assert column is None or isinstance(column, str)
1458
1551
  sc = self.filter(aggregation=operation, column=column)
1459
1552
  numaggs = await sc.length_async()
1460
1553
  assert numaggs <= 1
1461
1554
  if numaggs == 1:
1462
- return await sc.getitem_async(0)
1463
- else:
1464
- return await self.filter(realization=True).aggregate_async(
1465
- columns=[column] if column is not None else None,
1466
- operation=operation,
1467
- )
1555
+ agg = await sc.single_async
1556
+ assert isinstance(agg, objects.Child)
1557
+ ts = agg.metadata["_sumo"]["timestamp"]
1558
+ reals = await self.filter(
1559
+ realization=True,
1560
+ complex={"range": {"_sumo.timestamp": {"lt": ts}}},
1561
+ ).realizationids_async
1562
+ if set(reals) == set(
1563
+ agg.metadata["fmu"]["aggregation"]["realization_ids"]
1564
+ ):
1565
+ return agg
1566
+ # ELSE
1567
+ return await self.filter(realization=True).aggregate_async(
1568
+ columns=[column] if column is not None else None,
1569
+ operation=operation,
1570
+ )
1468
1571
 
1469
1572
  @deprecation.deprecated(
1470
1573
  details="Use the method 'aggregate' instead, with parameter 'operation'."
@@ -4,11 +4,11 @@ from typing import Dict
4
4
 
5
5
  from sumo.wrapper import SumoClient
6
6
 
7
- from fmu.sumo.explorer.objects._document import Document
8
- from fmu.sumo.explorer.objects._search_context import SearchContext
7
+ from ._document import Document
8
+ from ._search_context import SearchContext
9
9
 
10
10
 
11
- def _make_overview_query(id):
11
+ def _make_overview_query(id) -> Dict:
12
12
  return {
13
13
  "query": {"term": {"fmu.case.uuid.keyword": id}},
14
14
  "aggs": {
@@ -52,7 +52,7 @@ class Case(Document, SearchContext):
52
52
  self._iterations = None
53
53
 
54
54
  @property
55
- def overview(self):
55
+ def overview(self) -> Dict:
56
56
  """Overview of case contents."""
57
57
 
58
58
  def extract_bucket_keys(bucket, name):