climate-ref 0.5.0__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.
Files changed (44) hide show
  1. climate_ref/__init__.py +30 -0
  2. climate_ref/_config_helpers.py +214 -0
  3. climate_ref/alembic.ini +114 -0
  4. climate_ref/cli/__init__.py +138 -0
  5. climate_ref/cli/_utils.py +68 -0
  6. climate_ref/cli/config.py +28 -0
  7. climate_ref/cli/datasets.py +205 -0
  8. climate_ref/cli/executions.py +201 -0
  9. climate_ref/cli/providers.py +84 -0
  10. climate_ref/cli/solve.py +23 -0
  11. climate_ref/config.py +475 -0
  12. climate_ref/constants.py +8 -0
  13. climate_ref/database.py +223 -0
  14. climate_ref/dataset_registry/obs4ref_reference.txt +2 -0
  15. climate_ref/dataset_registry/sample_data.txt +60 -0
  16. climate_ref/datasets/__init__.py +40 -0
  17. climate_ref/datasets/base.py +214 -0
  18. climate_ref/datasets/cmip6.py +202 -0
  19. climate_ref/datasets/obs4mips.py +224 -0
  20. climate_ref/datasets/pmp_climatology.py +15 -0
  21. climate_ref/datasets/utils.py +16 -0
  22. climate_ref/executor/__init__.py +274 -0
  23. climate_ref/executor/local.py +89 -0
  24. climate_ref/migrations/README +22 -0
  25. climate_ref/migrations/env.py +139 -0
  26. climate_ref/migrations/script.py.mako +26 -0
  27. climate_ref/migrations/versions/2025-05-02T1418_341a4aa2551e_regenerate.py +292 -0
  28. climate_ref/models/__init__.py +33 -0
  29. climate_ref/models/base.py +42 -0
  30. climate_ref/models/dataset.py +206 -0
  31. climate_ref/models/diagnostic.py +61 -0
  32. climate_ref/models/execution.py +306 -0
  33. climate_ref/models/metric_value.py +195 -0
  34. climate_ref/models/provider.py +39 -0
  35. climate_ref/provider_registry.py +146 -0
  36. climate_ref/py.typed +0 -0
  37. climate_ref/solver.py +395 -0
  38. climate_ref/testing.py +109 -0
  39. climate_ref-0.5.0.dist-info/METADATA +97 -0
  40. climate_ref-0.5.0.dist-info/RECORD +44 -0
  41. climate_ref-0.5.0.dist-info/WHEEL +4 -0
  42. climate_ref-0.5.0.dist-info/entry_points.txt +2 -0
  43. climate_ref-0.5.0.dist-info/licenses/LICENCE +201 -0
  44. climate_ref-0.5.0.dist-info/licenses/NOTICE +3 -0
@@ -0,0 +1,206 @@
1
+ import datetime
2
+ from typing import Any, ClassVar
3
+
4
+ from sqlalchemy import ForeignKey, func
5
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
6
+
7
+ from climate_ref.models.base import Base
8
+ from climate_ref_core.datasets import SourceDatasetType
9
+
10
+
11
+ class Dataset(Base):
12
+ """
13
+ Represents a dataset
14
+
15
+ A dataset is a collection of data files, that is used as an input to the benchmarking process.
16
+ Adding/removing or updating a dataset will trigger a new diagnostic calculation.
17
+
18
+ A polymorphic association is used to capture the different types of datasets as each
19
+ dataset type may have different metadata fields.
20
+ This enables the use of a single table to store all datasets,
21
+ but still allows for querying specific metadata fields for each dataset type.
22
+ """
23
+
24
+ __tablename__ = "dataset"
25
+
26
+ id: Mapped[int] = mapped_column(primary_key=True)
27
+ slug: Mapped[str] = mapped_column(unique=True)
28
+ """
29
+ Globally unique identifier for the dataset.
30
+
31
+ In the case of CMIP6 datasets, this is the instance_id.
32
+ """
33
+ dataset_type: Mapped[SourceDatasetType] = mapped_column(nullable=False)
34
+ """
35
+ Type of dataset
36
+ """
37
+ created_at: Mapped[datetime.datetime] = mapped_column(server_default=func.now())
38
+ """
39
+ When the dataset was added to the database
40
+ """
41
+ updated_at: Mapped[datetime.datetime] = mapped_column(server_default=func.now(), onupdate=func.now())
42
+ """
43
+ When the dataset was updated.
44
+
45
+ Updating a dataset will trigger a new diagnostic calculation.
46
+ """
47
+
48
+ def __repr__(self) -> str:
49
+ return f"<Dataset slug={self.slug} dataset_type={self.dataset_type} >"
50
+
51
+ __mapper_args__: ClassVar[Any] = {"polymorphic_on": dataset_type} # type: ignore
52
+
53
+
54
+ class DatasetFile(Base):
55
+ """
56
+ Capture the metadata for a file in a dataset
57
+
58
+ A dataset may have multiple files, but is represented as a single dataset in the database.
59
+ A lot of the metadata will be duplicated for each file in the dataset,
60
+ but this will be more efficient for querying, filtering and building a data catalog.
61
+ """
62
+
63
+ __tablename__ = "dataset_file"
64
+
65
+ id: Mapped[int] = mapped_column(primary_key=True)
66
+ dataset_id: Mapped[int] = mapped_column(ForeignKey("dataset.id", ondelete="CASCADE"), nullable=False)
67
+ """
68
+ Foreign key to the dataset table
69
+ """
70
+
71
+ start_time: Mapped[datetime.datetime] = mapped_column(nullable=True)
72
+ """
73
+ Start time of a given file
74
+ """
75
+
76
+ end_time: Mapped[datetime.datetime] = mapped_column(nullable=True)
77
+ """
78
+ Start time of a given file
79
+ """
80
+
81
+ path: Mapped[str] = mapped_column()
82
+ """
83
+ Prefix that describes where the dataset is stored relative to the data directory
84
+ """
85
+
86
+ dataset = relationship("Dataset", backref="files")
87
+
88
+
89
+ class CMIP6Dataset(Dataset):
90
+ """
91
+ Represents a CMIP6 dataset
92
+
93
+ Fields that are not marked as required in
94
+ https://wcrp-cmip.github.io/WGCM_Infrastructure_Panel/Papers/CMIP6_global_attributes_filenames_CVs_v6.2.7.pdf
95
+ are optional.
96
+ """
97
+
98
+ __tablename__ = "cmip6_dataset"
99
+ id: Mapped[int] = mapped_column(ForeignKey("dataset.id"), primary_key=True)
100
+
101
+ activity_id: Mapped[str] = mapped_column()
102
+ branch_method: Mapped[str] = mapped_column(nullable=True)
103
+ branch_time_in_child: Mapped[float] = mapped_column(nullable=True)
104
+ branch_time_in_parent: Mapped[float] = mapped_column(nullable=True)
105
+ experiment: Mapped[str] = mapped_column()
106
+ experiment_id: Mapped[str] = mapped_column()
107
+ frequency: Mapped[str] = mapped_column()
108
+ grid: Mapped[str] = mapped_column()
109
+ grid_label: Mapped[str] = mapped_column()
110
+ institution_id: Mapped[str] = mapped_column()
111
+ long_name: Mapped[str] = mapped_column(nullable=True)
112
+ member_id: Mapped[str] = mapped_column()
113
+ nominal_resolution: Mapped[str] = mapped_column()
114
+ parent_activity_id: Mapped[str] = mapped_column(nullable=True)
115
+ parent_experiment_id: Mapped[str] = mapped_column(nullable=True)
116
+ parent_source_id: Mapped[str] = mapped_column(nullable=True)
117
+ parent_time_units: Mapped[str] = mapped_column(nullable=True)
118
+ parent_variant_label: Mapped[str] = mapped_column(nullable=True)
119
+ realm: Mapped[str] = mapped_column()
120
+ product: Mapped[str] = mapped_column()
121
+ source_id: Mapped[str] = mapped_column()
122
+ standard_name: Mapped[str] = mapped_column()
123
+ source_type: Mapped[str] = mapped_column()
124
+ sub_experiment: Mapped[str] = mapped_column()
125
+ sub_experiment_id: Mapped[str] = mapped_column()
126
+ table_id: Mapped[str] = mapped_column()
127
+ units: Mapped[str] = mapped_column()
128
+ variable_id: Mapped[str] = mapped_column()
129
+ variant_label: Mapped[str] = mapped_column()
130
+ vertical_levels: Mapped[int] = mapped_column(nullable=True)
131
+ version: Mapped[str] = mapped_column()
132
+
133
+ instance_id: Mapped[str] = mapped_column()
134
+ """
135
+ Unique identifier for the dataset.
136
+ """
137
+
138
+ __mapper_args__: ClassVar[Any] = {"polymorphic_identity": SourceDatasetType.CMIP6} # type: ignore
139
+
140
+
141
+ class Obs4MIPsDataset(Dataset):
142
+ """
143
+ Represents a obs4mips dataset
144
+
145
+ TODO: Should the metadata fields be part of the file or dataset?
146
+ """
147
+
148
+ __tablename__ = "obs4mips_dataset"
149
+ id: Mapped[int] = mapped_column(ForeignKey("dataset.id"), primary_key=True)
150
+
151
+ activity_id: Mapped[str] = mapped_column()
152
+ frequency: Mapped[str] = mapped_column()
153
+ grid: Mapped[str] = mapped_column()
154
+ grid_label: Mapped[str] = mapped_column()
155
+ institution_id: Mapped[str] = mapped_column()
156
+ long_name: Mapped[str] = mapped_column()
157
+ nominal_resolution: Mapped[str] = mapped_column()
158
+ realm: Mapped[str] = mapped_column()
159
+ product: Mapped[str] = mapped_column()
160
+ source_id: Mapped[str] = mapped_column()
161
+ source_type: Mapped[str] = mapped_column()
162
+ units: Mapped[str] = mapped_column()
163
+ variable_id: Mapped[str] = mapped_column()
164
+ variant_label: Mapped[str] = mapped_column()
165
+ vertical_levels: Mapped[int] = mapped_column()
166
+ source_version_number: Mapped[str] = mapped_column()
167
+
168
+ instance_id: Mapped[str] = mapped_column()
169
+ """
170
+ Unique identifier for the dataset.
171
+ """
172
+ __mapper_args__: ClassVar[Any] = {"polymorphic_identity": SourceDatasetType.obs4MIPs} # type: ignore
173
+
174
+
175
+ class PMPClimatologyDataset(Dataset):
176
+ """
177
+ Represents a climatology dataset from PMP
178
+
179
+ These data are similar to obs4MIPs datasets, but are post-processed
180
+ """
181
+
182
+ __tablename__ = "pmp_climatology_dataset"
183
+ id: Mapped[int] = mapped_column(ForeignKey("dataset.id"), primary_key=True)
184
+
185
+ activity_id: Mapped[str] = mapped_column()
186
+ frequency: Mapped[str] = mapped_column()
187
+ grid: Mapped[str] = mapped_column()
188
+ grid_label: Mapped[str] = mapped_column()
189
+ institution_id: Mapped[str] = mapped_column()
190
+ long_name: Mapped[str] = mapped_column()
191
+ nominal_resolution: Mapped[str] = mapped_column()
192
+ realm: Mapped[str] = mapped_column()
193
+ product: Mapped[str] = mapped_column()
194
+ source_id: Mapped[str] = mapped_column()
195
+ source_type: Mapped[str] = mapped_column()
196
+ units: Mapped[str] = mapped_column()
197
+ variable_id: Mapped[str] = mapped_column()
198
+ variant_label: Mapped[str] = mapped_column()
199
+ vertical_levels: Mapped[int] = mapped_column()
200
+ source_version_number: Mapped[str] = mapped_column()
201
+
202
+ instance_id: Mapped[str] = mapped_column()
203
+ """
204
+ Unique identifier for the dataset.
205
+ """
206
+ __mapper_args__: ClassVar[Any] = {"polymorphic_identity": SourceDatasetType.PMPClimatology} # type: ignore
@@ -0,0 +1,61 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from sqlalchemy import ForeignKey, UniqueConstraint
4
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
5
+
6
+ from climate_ref.models.base import Base, CreatedUpdatedMixin
7
+
8
+ if TYPE_CHECKING:
9
+ from climate_ref.models.execution import ExecutionGroup
10
+ from climate_ref.models.provider import Provider
11
+
12
+
13
+ class Diagnostic(CreatedUpdatedMixin, Base):
14
+ """
15
+ Represents a diagnostic that can be calculated
16
+ """
17
+
18
+ __tablename__ = "diagnostic"
19
+ __table_args__ = (UniqueConstraint("provider_id", "slug", name="diagnostic_ident"),)
20
+
21
+ id: Mapped[int] = mapped_column(primary_key=True)
22
+ slug: Mapped[str] = mapped_column(unique=True)
23
+ """
24
+ Unique identifier for the diagnostic
25
+
26
+ This will be used to reference the diagnostic in the benchmarking process
27
+ """
28
+
29
+ name: Mapped[str] = mapped_column()
30
+ """
31
+ Long name of the diagnostic
32
+ """
33
+
34
+ provider_id: Mapped[int] = mapped_column(ForeignKey("provider.id"))
35
+ """
36
+ The provider that provides the diagnostic
37
+ """
38
+
39
+ enabled: Mapped[bool] = mapped_column(default=True)
40
+ """
41
+ Whether the diagnostic is enabled or not
42
+
43
+ If a diagnostic is not enabled, it will not be used for any calculations.
44
+ """
45
+
46
+ provider: Mapped["Provider"] = relationship(back_populates="diagnostics")
47
+ execution_groups: Mapped[list["ExecutionGroup"]] = relationship(back_populates="diagnostic")
48
+
49
+ def __repr__(self) -> str:
50
+ return f"<Metric slug={self.slug}>"
51
+
52
+ def full_slug(self) -> str:
53
+ """
54
+ Get the full slug of the diagnostic, including the provider slug
55
+
56
+ Returns
57
+ -------
58
+ str
59
+ Full slug of the diagnostic
60
+ """
61
+ return f"{self.provider.slug}/{self.slug}"
@@ -0,0 +1,306 @@
1
+ import enum
2
+ import pathlib
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from loguru import logger
6
+ from sqlalchemy import Column, ForeignKey, Table, UniqueConstraint, func
7
+ from sqlalchemy.orm import Mapped, Session, mapped_column, relationship
8
+ from sqlalchemy.orm.query import RowReturningQuery
9
+
10
+ from climate_ref.models import Dataset
11
+ from climate_ref.models.base import Base, CreatedUpdatedMixin
12
+ from climate_ref_core.datasets import ExecutionDatasetCollection
13
+
14
+ if TYPE_CHECKING:
15
+ from climate_ref.database import Database
16
+ from climate_ref.models.diagnostic import Diagnostic
17
+ from climate_ref.models.metric_value import MetricValue
18
+
19
+
20
+ class ExecutionGroup(CreatedUpdatedMixin, Base):
21
+ """
22
+ Represents a group of executions with a shared set of input datasets.
23
+
24
+ When solving, the `ExecutionGroup`s are derived from the available datasets,
25
+ the defined diagnostics and their data requirements. From the information in the
26
+ group an execution can be triggered, which is an actual run of a diagnostic calculation
27
+ with a specific set of input datasets.
28
+
29
+ When the `ExecutionGroup` is created, it is marked dirty, meaning there are no
30
+ current executions available. When an Execution was run successfully for a
31
+ ExecutionGroup, the dirty mark is removed. After ingesting new data and
32
+ solving again and if new versions of the input datasets are available, the
33
+ ExecutionGroup will be marked dirty again.
34
+
35
+ The diagnostic_id and key form a unique identifier for `ExecutionGroup`s.
36
+ """
37
+
38
+ __tablename__ = "execution_group"
39
+ __table_args__ = (UniqueConstraint("diagnostic_id", "key", name="execution_ident"),)
40
+
41
+ id: Mapped[int] = mapped_column(primary_key=True)
42
+
43
+ diagnostic_id: Mapped[int] = mapped_column(ForeignKey("diagnostic.id"))
44
+ """
45
+ The diagnostic that this execution group belongs to
46
+ """
47
+
48
+ key: Mapped[str] = mapped_column(index=True)
49
+ """
50
+ Key for the datasets in this Execution group.
51
+ """
52
+
53
+ dirty: Mapped[bool] = mapped_column(default=False)
54
+ """
55
+ Whether the execution group should be rerun
56
+
57
+ An execution group is dirty if the diagnostic or any of the input datasets has been
58
+ updated since the last execution.
59
+ """
60
+
61
+ selectors: Mapped[dict[str, Any]] = mapped_column(default=dict)
62
+ """
63
+ Collection of selectors that define the group
64
+
65
+ These selectors are the unique key, value pairs that were selected during the initial groupby
66
+ operation.
67
+ These are also used to define the dataset key.
68
+ """
69
+
70
+ diagnostic: Mapped["Diagnostic"] = relationship(back_populates="execution_groups")
71
+ executions: Mapped[list["Execution"]] = relationship(
72
+ back_populates="execution_group", order_by="Execution.created_at"
73
+ )
74
+
75
+ def should_run(self, dataset_hash: str) -> bool:
76
+ """
77
+ Check if the diagnostic execution group needs to be executed.
78
+
79
+ The diagnostic execution group should be run if:
80
+
81
+ * the execution group is marked as dirty
82
+ * no executions have been performed ever
83
+ * the dataset hash is different from the last run
84
+ """
85
+ if not self.executions:
86
+ logger.debug(f"Execution group {self.diagnostic.slug}/{self.key} was never executed")
87
+ return True
88
+
89
+ if self.executions[-1].dataset_hash != dataset_hash:
90
+ logger.debug(
91
+ f"Execution group {self.diagnostic.slug}/{self.key} hash mismatch:"
92
+ f" {self.executions[-1].dataset_hash} != {dataset_hash}"
93
+ )
94
+ return True
95
+
96
+ if self.dirty:
97
+ logger.debug(f"Execution group {self.diagnostic.slug}/{self.key} is dirty")
98
+ return True
99
+
100
+ return False
101
+
102
+
103
+ execution_datasets = Table(
104
+ "execution_dataset",
105
+ Base.metadata,
106
+ Column("execution_id", ForeignKey("execution.id")),
107
+ Column("dataset_id", ForeignKey("dataset.id")),
108
+ )
109
+
110
+
111
+ class Execution(CreatedUpdatedMixin, Base):
112
+ """
113
+ Represents a single execution of a diagnostic
114
+
115
+ Each result is part of a group of executions that share similar input datasets.
116
+
117
+ An execution group might be run multiple times as new data becomes available,
118
+ each run will create a `Execution`.
119
+ """
120
+
121
+ __tablename__ = "execution"
122
+
123
+ id: Mapped[int] = mapped_column(primary_key=True)
124
+
125
+ output_fragment: Mapped[str] = mapped_column()
126
+ """
127
+ Relative directory to store the output of the execution.
128
+
129
+ During execution this directory is relative to the temporary directory.
130
+ If the diagnostic execution is successful, the executions will be moved to the final output directory
131
+ and the temporary directory will be cleaned up.
132
+ This directory may contain multiple input and output files.
133
+ """
134
+
135
+ execution_group_id: Mapped[int] = mapped_column(
136
+ ForeignKey(
137
+ "execution_group.id",
138
+ name="fk_execution_id",
139
+ )
140
+ )
141
+ """
142
+ The execution group that this execution belongs to
143
+ """
144
+
145
+ dataset_hash: Mapped[str] = mapped_column(index=True)
146
+ """
147
+ Hash of the datasets used to calculate the diagnostic
148
+
149
+ This is used to verify if an existing diagnostic execution has been run with the same datasets.
150
+ """
151
+
152
+ successful: Mapped[bool] = mapped_column(nullable=True)
153
+ """
154
+ Was the run successful
155
+ """
156
+
157
+ path: Mapped[str] = mapped_column(nullable=True)
158
+ """
159
+ Path to the output bundle
160
+
161
+ Relative to the diagnostic execution result output directory
162
+ """
163
+
164
+ retracted: Mapped[bool] = mapped_column(default=False)
165
+ """
166
+ Whether the diagnostic execution result has been retracted or not
167
+
168
+ This may happen if a dataset has been retracted, or if the diagnostic execution was incorrect.
169
+ Rather than delete the values, they are marked as retracted.
170
+ These data may still be visible in the UI, but should be marked as retracted.
171
+ """
172
+
173
+ execution_group: Mapped["ExecutionGroup"] = relationship(back_populates="executions")
174
+ outputs: Mapped[list["ExecutionOutput"]] = relationship(back_populates="execution")
175
+ values: Mapped[list["MetricValue"]] = relationship(back_populates="execution")
176
+
177
+ datasets: Mapped[list[Dataset]] = relationship(secondary=execution_datasets)
178
+ """
179
+ The datasets used in this execution
180
+ """
181
+
182
+ def register_datasets(self, db: "Database", execution_dataset: ExecutionDatasetCollection) -> None:
183
+ """
184
+ Register the datasets used in the diagnostic calculation with the execution
185
+ """
186
+ for _, dataset in execution_dataset.items():
187
+ db.session.execute(
188
+ execution_datasets.insert(),
189
+ [{"execution_id": self.id, "dataset_id": idx} for idx in dataset.index],
190
+ )
191
+
192
+ def mark_successful(self, path: pathlib.Path | str) -> None:
193
+ """
194
+ Mark the diagnostic execution as successful
195
+ """
196
+ # TODO: this needs to accept both a diagnostic and output bundle
197
+ self.successful = True
198
+ self.path = str(path)
199
+
200
+ def mark_failed(self) -> None:
201
+ """
202
+ Mark the diagnostic execution as unsuccessful
203
+ """
204
+ self.successful = False
205
+
206
+
207
+ class ResultOutputType(enum.Enum):
208
+ """
209
+ Types of supported outputs
210
+
211
+ These map to the categories of output in the CMEC output bundle
212
+ """
213
+
214
+ Plot = "plot"
215
+ Data = "data"
216
+ HTML = "html"
217
+
218
+
219
+ class ExecutionOutput(CreatedUpdatedMixin, Base):
220
+ """
221
+ An output generated as part of an execution.
222
+
223
+ This output may be a plot, data file or HTML file.
224
+ These outputs are defined in the CMEC output bundle
225
+ """
226
+
227
+ __tablename__ = "execution_output"
228
+
229
+ id: Mapped[int] = mapped_column(primary_key=True)
230
+
231
+ execution_id: Mapped[int] = mapped_column(ForeignKey("execution.id"), index=True)
232
+
233
+ output_type: Mapped[ResultOutputType] = mapped_column(index=True)
234
+ """
235
+ Type of the output
236
+
237
+ This will determine how the output is displayed
238
+ """
239
+
240
+ filename: Mapped[str] = mapped_column(nullable=True)
241
+ """
242
+ Path to the output
243
+
244
+ Relative to the diagnostic execution result output directory
245
+ """
246
+
247
+ short_name: Mapped[str] = mapped_column(nullable=True)
248
+ """
249
+ Short key of the output
250
+
251
+ This is unique for a given result and output type
252
+ """
253
+
254
+ long_name: Mapped[str] = mapped_column(nullable=True)
255
+ """
256
+ Human readable name describing the plot
257
+ """
258
+
259
+ description: Mapped[str] = mapped_column(nullable=True)
260
+ """
261
+ Long description describing the plot
262
+ """
263
+
264
+ execution: Mapped["Execution"] = relationship(back_populates="outputs")
265
+
266
+
267
+ def get_execution_group_and_latest(
268
+ session: Session,
269
+ ) -> RowReturningQuery[tuple[ExecutionGroup, Execution | None]]:
270
+ """
271
+ Query to get the most recent result for each execution group
272
+
273
+ Parameters
274
+ ----------
275
+ session
276
+ The database session to use for the query.
277
+
278
+ Returns
279
+ -------
280
+ Query to get the most recent result for each execution group.
281
+ The result is a tuple of the execution group and the most recent result,
282
+ which can be None.
283
+ """
284
+ # Find the most recent result for each execution group
285
+ # This uses an aggregate function because it is more efficient than order by
286
+ subquery = (
287
+ session.query(
288
+ Execution.execution_group_id,
289
+ func.max(Execution.created_at).label("latest_created_at"),
290
+ )
291
+ .group_by(Execution.execution_group_id)
292
+ .subquery()
293
+ )
294
+
295
+ # Join the diagnostic execution with the latest result
296
+ query = (
297
+ session.query(ExecutionGroup, Execution)
298
+ .outerjoin(subquery, ExecutionGroup.id == subquery.c.execution_group_id)
299
+ .outerjoin(
300
+ Execution,
301
+ (Execution.execution_group_id == ExecutionGroup.id)
302
+ & (Execution.created_at == subquery.c.latest_created_at),
303
+ )
304
+ )
305
+
306
+ return query # type: ignore