cognite-neat 1.0.10__py3-none-any.whl → 1.0.12__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.
@@ -3,13 +3,13 @@ import sys
3
3
  from abc import ABC, abstractmethod
4
4
  from collections import UserList, defaultdict
5
5
  from collections.abc import Hashable, Sequence
6
- from datetime import datetime
7
6
  from enum import Enum
8
7
  from typing import Any, Generic, Literal, TypeAlias, cast
9
8
 
10
9
  from pydantic import BaseModel, Field
11
10
  from pydantic.alias_generators import to_camel
12
11
 
12
+ from cognite.neat._data_model._snapshot import SchemaSnapshot
13
13
  from cognite.neat._data_model.models.dms import (
14
14
  BaseModelObject,
15
15
  Constraint,
@@ -18,16 +18,11 @@ from cognite.neat._data_model.models.dms import (
18
18
  ContainerPropertyDefinition,
19
19
  ContainerReference,
20
20
  ContainerRequest,
21
- DataModelReference,
22
21
  DataModelRequest,
23
22
  DataModelResource,
24
23
  Index,
25
- NodeReference,
26
- SpaceReference,
27
- SpaceRequest,
28
24
  T_DataModelResource,
29
25
  T_ResourceId,
30
- ViewReference,
31
26
  ViewRequest,
32
27
  ViewRequestProperty,
33
28
  )
@@ -239,15 +234,6 @@ class ContainerDeploymentPlan(ResourceDeploymentPlan[ContainerReference, Contain
239
234
  return False
240
235
 
241
236
 
242
- class SchemaSnapshot(BaseDeployObject):
243
- timestamp: datetime
244
- data_model: dict[DataModelReference, DataModelRequest]
245
- views: dict[ViewReference, ViewRequest]
246
- containers: dict[ContainerReference, ContainerRequest]
247
- spaces: dict[SpaceReference, SpaceRequest]
248
- node_types: dict[NodeReference, NodeReference]
249
-
250
-
251
237
  class ResourceDeploymentPlanList(UserList[ResourceDeploymentPlan]):
252
238
  def consolidate_changes(self) -> Self:
253
239
  """Consolidate the deployment plans by applying field removals to the new_value of resources."""
@@ -1,11 +1,11 @@
1
1
  from collections.abc import Callable, Mapping, Sequence
2
2
  from dataclasses import dataclass
3
- from datetime import datetime, timezone
4
3
  from functools import partial
5
4
  from typing import cast
6
5
 
7
6
  from cognite.neat._client import NeatClient
8
7
  from cognite.neat._data_model._shared import OnSuccessResultProducer
8
+ from cognite.neat._data_model._snapshot import SchemaSnapshot
9
9
  from cognite.neat._data_model.models.dms import (
10
10
  ContainerReference,
11
11
  ContainerRequest,
@@ -46,7 +46,6 @@ from .data_classes import (
46
46
  ResourceChange,
47
47
  ResourceDeploymentPlan,
48
48
  ResourceDeploymentPlanList,
49
- SchemaSnapshot,
50
49
  SeverityType,
51
50
  )
52
51
 
@@ -88,7 +87,7 @@ class SchemaDeployer(OnSuccessResultProducer):
88
87
 
89
88
  def deploy(self, data_model: RequestSchema) -> DeploymentResult:
90
89
  # Step 1: Fetch current CDF state
91
- snapshot = self.fetch_cdf_state(data_model)
90
+ snapshot = SchemaSnapshot.fetch_cdf_data_model(self.client, data_model)
92
91
 
93
92
  # Step 2: Create deployment plan by comparing local vs cdf
94
93
  plan = self.create_deployment_plan(snapshot, data_model)
@@ -128,30 +127,6 @@ class SchemaDeployer(OnSuccessResultProducer):
128
127
  status="success" if changes.is_success else "partial", plan=list(plan), snapshot=snapshot, responses=changes
129
128
  )
130
129
 
131
- def fetch_cdf_state(self, data_model: RequestSchema) -> SchemaSnapshot:
132
- now = datetime.now(tz=timezone.utc)
133
- space_ids = [space.as_reference() for space in data_model.spaces]
134
- cdf_spaces = self.client.spaces.retrieve(space_ids)
135
-
136
- container_refs = [c.as_reference() for c in data_model.containers]
137
- cdf_containers = self.client.containers.retrieve(container_refs)
138
-
139
- view_refs = [v.as_reference() for v in data_model.views]
140
- cdf_views = self.client.views.retrieve(view_refs)
141
-
142
- dm_ref = data_model.data_model.as_reference()
143
- cdf_data_models = self.client.data_models.retrieve([dm_ref])
144
-
145
- nodes = [node_type for view in cdf_views for node_type in view.node_types]
146
- return SchemaSnapshot(
147
- timestamp=now,
148
- data_model={dm.as_reference(): dm.as_request() for dm in cdf_data_models},
149
- views={view.as_reference(): view.as_request() for view in cdf_views},
150
- containers={container.as_reference(): container.as_request() for container in cdf_containers},
151
- spaces={space.as_reference(): space.as_request() for space in cdf_spaces},
152
- node_types={node: node for node in nodes},
153
- )
154
-
155
130
  def create_deployment_plan(self, snapshot: SchemaSnapshot, data_model: RequestSchema) -> ResourceDeploymentPlanList:
156
131
  return ResourceDeploymentPlanList(
157
132
  [
@@ -114,6 +114,9 @@ class ContainerDirectReference(ReferenceObject):
114
114
  pattern=CONTAINER_AND_VIEW_PROPERTIES_IDENTIFIER_PATTERN,
115
115
  )
116
116
 
117
+ def __str__(self) -> str:
118
+ return f"{self.source!s}.{self.identifier}"
119
+
117
120
 
118
121
  class ViewDirectReference(ReferenceObject):
119
122
  source: ViewReference = Field(
@@ -126,6 +129,9 @@ class ViewDirectReference(ReferenceObject):
126
129
  pattern=CONTAINER_AND_VIEW_PROPERTIES_IDENTIFIER_PATTERN,
127
130
  )
128
131
 
132
+ def __str__(self) -> str:
133
+ return f"{self.source!s}.{self.identifier}"
134
+
129
135
 
130
136
  class ContainerIndexReference(ContainerReference):
131
137
  identifier: str
@@ -21,6 +21,7 @@ from ._references import ContainerReference, NodeReference, ViewReference
21
21
  from ._view_filter import Filter
22
22
  from ._view_property import (
23
23
  EdgeProperty,
24
+ ViewCorePropertyRequest,
24
25
  ViewCorePropertyResponse,
25
26
  ViewRequestProperty,
26
27
  ViewResponseProperty,
@@ -130,6 +131,11 @@ class ViewRequest(View):
130
131
  """Validate properties Identifier"""
131
132
  return _validate_properties_keys(val)
132
133
 
134
+ @property
135
+ def used_containers(self) -> set[ContainerReference]:
136
+ """Get all containers referenced by this view."""
137
+ return {prop.container for prop in self.properties.values() if isinstance(prop, ViewCorePropertyRequest)}
138
+
133
139
 
134
140
  class ViewResponse(View, WriteableResource[ViewRequest]):
135
141
  properties: dict[str, ViewResponseProperty] = Field(
@@ -29,7 +29,7 @@ class DataModelMissingName(DataModelValidator):
29
29
  def run(self) -> list[Recommendation]:
30
30
  recommendations: list[Recommendation] = []
31
31
 
32
- if not self.local_resources.data_model_name:
32
+ if not self.validation_resources.merged_data_model.name:
33
33
  recommendations.append(
34
34
  Recommendation(
35
35
  message="Data model is missing a human-readable name.",
@@ -68,7 +68,7 @@ class DataModelMissingDescription(DataModelValidator):
68
68
  def run(self) -> list[Recommendation]:
69
69
  recommendations: list[Recommendation] = []
70
70
 
71
- if not self.local_resources.data_model_description:
71
+ if not self.validation_resources.merged_data_model.description:
72
72
  recommendations.append(
73
73
  Recommendation(
74
74
  message="Data model is missing a description.",
@@ -103,11 +103,11 @@ class ViewMissingName(DataModelValidator):
103
103
  def run(self) -> list[Recommendation]:
104
104
  recommendations: list[Recommendation] = []
105
105
 
106
- for view_ref in self.local_resources.data_model_views:
107
- view = self.local_resources.views_by_reference.get(view_ref)
106
+ for view_ref in self.validation_resources.merged_data_model.views or []:
107
+ view = self.validation_resources.select_view(view_ref)
108
108
 
109
109
  if view is None:
110
- raise RuntimeError(f"View {view_ref!s} not found in local resources. This is a bug.")
110
+ raise RuntimeError(f"ViewMissingName.run: View {view_ref!s} not found. This is a bug.")
111
111
 
112
112
  if not view.name:
113
113
  recommendations.append(
@@ -154,11 +154,11 @@ class ViewMissingDescription(DataModelValidator):
154
154
  def run(self) -> list[Recommendation]:
155
155
  recommendations: list[Recommendation] = []
156
156
 
157
- for view_ref in self.local_resources.data_model_views:
158
- view = self.local_resources.views_by_reference.get(view_ref)
157
+ for view_ref in self.validation_resources.merged_data_model.views or []:
158
+ view = self.validation_resources.select_view(view_ref)
159
159
 
160
160
  if view is None:
161
- raise RuntimeError(f"View {view_ref!s} not found in local resources. This is a bug.")
161
+ raise RuntimeError(f"ViewMissingDescription.run: View {view_ref!s} not found. This is a bug.")
162
162
 
163
163
  if not view.description:
164
164
  recommendations.append(
@@ -195,17 +195,24 @@ class ViewPropertyMissingName(DataModelValidator):
195
195
  def run(self) -> list[Recommendation]:
196
196
  recommendations: list[Recommendation] = []
197
197
 
198
- for view_ref in self.local_resources.data_model_views:
199
- if properties := self.local_resources.properties_by_view.get(view_ref):
200
- for prop_ref, definition in properties.items():
201
- if not definition.name:
202
- recommendations.append(
203
- Recommendation(
204
- message=f"View {view_ref!s} property {prop_ref!s} is missing a human-readable name.",
205
- fix="Add a clear and concise name to the view property.",
206
- code=self.code,
207
- )
198
+ for view_ref in self.validation_resources.merged_data_model.views or []:
199
+ view = self.validation_resources.select_view(view_ref)
200
+
201
+ if view is None:
202
+ raise RuntimeError(f"ViewMissingName.run: View {view_ref!s} not found. This is a bug.")
203
+
204
+ if not view.properties:
205
+ continue
206
+
207
+ for prop_ref, definition in view.properties.items():
208
+ if not definition.name:
209
+ recommendations.append(
210
+ Recommendation(
211
+ message=f"View {view_ref!s} property {prop_ref!s} is missing a human-readable name.",
212
+ fix="Add a clear and concise name to the view property.",
213
+ code=self.code,
208
214
  )
215
+ )
209
216
 
210
217
  return recommendations
211
218
 
@@ -244,17 +251,24 @@ class ViewPropertyMissingDescription(DataModelValidator):
244
251
  def run(self) -> list[Recommendation]:
245
252
  recommendations: list[Recommendation] = []
246
253
 
247
- for view_ref in self.local_resources.data_model_views:
248
- if properties := self.local_resources.properties_by_view.get(view_ref):
249
- for prop_ref, definition in properties.items():
250
- if not definition.description:
251
- recommendations.append(
252
- Recommendation(
253
- message=f"View {view_ref!s} property {prop_ref!s} is missing a description.",
254
- fix="Add a clear and concise description to the view property.",
255
- code=self.code,
256
- )
254
+ for view_ref in self.validation_resources.merged_data_model.views or []:
255
+ view = self.validation_resources.select_view(view_ref)
256
+
257
+ if view is None:
258
+ raise RuntimeError(f"ViewMissingName.run: View {view_ref!s} not found. This is a bug.")
259
+
260
+ if not view.properties:
261
+ continue
262
+
263
+ for prop_ref, definition in view.properties.items():
264
+ if not definition.description:
265
+ recommendations.append(
266
+ Recommendation(
267
+ message=f"View {view_ref!s} property {prop_ref!s} is missing a description.",
268
+ fix="Add a clear and concise description to the view property.",
269
+ code=self.code,
257
270
  )
271
+ )
258
272
 
259
273
  return recommendations
260
274
 
@@ -282,10 +296,7 @@ class EnumerationMissingName(DataModelValidator):
282
296
  def run(self) -> list[Recommendation]:
283
297
  recommendations: list[Recommendation] = []
284
298
 
285
- for container_ref, container in self.local_resources.containers_by_reference.items():
286
- if container_ref.space != self.local_resources.data_model_reference.space:
287
- continue
288
-
299
+ for container_ref, container in self.validation_resources.merged.containers.items():
289
300
  for prop_ref, definition in container.properties.items():
290
301
  if not isinstance(definition.type, EnumProperty):
291
302
  continue
@@ -340,10 +351,7 @@ class EnumerationMissingDescription(DataModelValidator):
340
351
  def run(self) -> list[Recommendation]:
341
352
  recommendations: list[Recommendation] = []
342
353
 
343
- for container_ref, container in self.local_resources.containers_by_reference.items():
344
- if container_ref.space != self.local_resources.data_model_reference.space:
345
- continue
346
-
354
+ for container_ref, container in self.validation_resources.merged.containers.items():
347
355
  for prop_ref, definition in container.properties.items():
348
356
  if not isinstance(definition.type, EnumProperty):
349
357
  continue
@@ -1,57 +1,8 @@
1
1
  from abc import ABC, abstractmethod
2
- from dataclasses import dataclass
3
- from itertools import chain
4
- from typing import ClassVar, TypeAlias, cast
2
+ from typing import ClassVar
5
3
 
6
- from pyparsing import cached_property
7
-
8
- from cognite.neat._data_model.models.dms._container import ContainerRequest
9
- from cognite.neat._data_model.models.dms._references import (
10
- ContainerDirectReference,
11
- ContainerReference,
12
- DataModelReference,
13
- ViewDirectReference,
14
- ViewReference,
15
- )
16
- from cognite.neat._data_model.models.dms._view_property import ViewRequestProperty
17
- from cognite.neat._data_model.models.dms._views import ViewRequest
4
+ from cognite.neat._data_model._analysis import ValidationResources
18
5
  from cognite.neat._issues import ConsistencyError, Recommendation
19
- from cognite.neat._utils.useful_types import ModusOperandi
20
-
21
- # Type aliases for better readability
22
- ViewsByReference: TypeAlias = dict[ViewReference, ViewRequest]
23
- ContainersByReference: TypeAlias = dict[ContainerReference, ContainerRequest]
24
- AncestorsByReference: TypeAlias = dict[ViewReference, set[ViewReference]]
25
- ReverseToDirectMapping: TypeAlias = dict[
26
- tuple[ViewReference, str], tuple[ViewReference, ContainerDirectReference | ViewDirectReference]
27
- ]
28
- ConnectionEndNodeTypes: TypeAlias = dict[tuple[ViewReference, str], ViewReference | None]
29
-
30
-
31
- @dataclass
32
- class LocalResources:
33
- """Local data model resources."""
34
-
35
- data_model_reference: DataModelReference
36
- data_model_views: set[ViewReference]
37
- data_model_description: str | None
38
- data_model_name: str | None
39
- views_by_reference: ViewsByReference
40
- properties_by_view: dict[ViewReference, dict[str, ViewRequestProperty]]
41
- ancestors_by_view_reference: AncestorsByReference
42
- reverse_to_direct_mapping: ReverseToDirectMapping
43
- containers_by_reference: ContainersByReference
44
- connection_end_node_types: ConnectionEndNodeTypes
45
-
46
-
47
- @dataclass
48
- class CDFResources:
49
- """CDF resources."""
50
-
51
- views_by_reference: ViewsByReference
52
- ancestors_by_view_reference: AncestorsByReference
53
- containers_by_reference: ContainersByReference
54
- data_model_views: set[ViewReference]
55
6
 
56
7
 
57
8
  class DataModelValidator(ABC):
@@ -62,288 +13,12 @@ class DataModelValidator(ABC):
62
13
 
63
14
  def __init__(
64
15
  self,
65
- local_resources: LocalResources,
66
- cdf_resources: CDFResources,
67
- modus_operandi: ModusOperandi = "additive",
16
+ validation_resources: ValidationResources,
68
17
  ) -> None:
69
- self.local_resources = local_resources
70
- self.cdf_resources = cdf_resources
71
- self.modus_operandi = modus_operandi
18
+ self.validation_resources = validation_resources
72
19
 
73
20
  @abstractmethod
74
21
  def run(self) -> list[ConsistencyError] | list[Recommendation] | list[ConsistencyError | Recommendation]:
75
22
  """Execute the success handler on the data model."""
76
23
  # do something with data model
77
24
  ...
78
-
79
- def _select_view_with_property(self, view_ref: ViewReference, property_: str) -> ViewRequest | None:
80
- """Select the appropriate view (local or CDF) that contains desired property.
81
-
82
- Prioritizes views that contain the property (first local than CDF),
83
- then falls back to any available view (even without the property).
84
-
85
- Args:
86
- view_ref: Reference to the view.
87
- property_: Property name to look for.
88
-
89
- Returns:
90
- The selected ViewRequest if found, else None.
91
-
92
- !! note "Behavior based on modus operandi"
93
- - In "additive" modus operandi, local and CDF view will be considered irrirespective of their space.
94
- - In "rebuild" modus operandi, local views will be considered irrispective of their space, while CDF views
95
- will only be considered if they belong to the different space than the local data model space
96
- (as they are considered external resources that is managed under other data model/schema space).
97
-
98
- """
99
-
100
- local_view = self.local_resources.views_by_reference.get(view_ref)
101
- cdf_view = (
102
- self.cdf_resources.views_by_reference.get(view_ref)
103
- if view_ref.space != self.local_resources.data_model_reference.space or self.modus_operandi == "additive"
104
- else None
105
- )
106
-
107
- # Try views with the property first, then any available view
108
- candidates = chain(
109
- (v for v in (local_view, cdf_view) if v and v.properties and property_ in v.properties),
110
- (v for v in (local_view, cdf_view) if v),
111
- )
112
-
113
- return next(candidates, None)
114
-
115
- def _select_container_with_property(
116
- self, container_ref: ContainerReference, property_: str
117
- ) -> ContainerRequest | None:
118
- """Select the appropriate container (local or CDF) that contains the desired property.
119
-
120
- Prioritizes containers that contain the property (first local than CDF),
121
- then falls back to any available container.
122
-
123
- Args:
124
- container_ref: Reference to the container.
125
- property_: Property name to look for.
126
-
127
- Returns:
128
- The selected ContainerRequest if found, else None.
129
-
130
- !! note "Behavior based on modus operandi"
131
- - In "additive" modus operandi, local and CDF containers will be considered irrirespective of their space.
132
- - In "rebuild" modus operandi, local containers will be considered irrispective of their space, while CDF
133
- containers will only be considered if they belong to the different space than the local data model space
134
- (as they are considered external resources that is managed under other data model/schema space).
135
-
136
- """
137
- local_container = self.local_resources.containers_by_reference.get(container_ref)
138
- cdf_container = self.cdf_resources.containers_by_reference.get(container_ref)
139
-
140
- cdf_container = (
141
- self.cdf_resources.containers_by_reference.get(container_ref)
142
- if container_ref.space != self.local_resources.data_model_reference.space
143
- or self.modus_operandi == "additive"
144
- else None
145
- )
146
-
147
- # Try containers with the property first, then any available container
148
- candidates = chain(
149
- (c for c in (local_container, cdf_container) if c and c.properties and property_ in c.properties),
150
- (c for c in (local_container, cdf_container) if c),
151
- )
152
-
153
- return next(candidates, None)
154
-
155
- @cached_property
156
- def data_model_view_references(self) -> set[ViewReference]:
157
- """Get all data model view references to validate based on deployment mode.
158
-
159
- In "rebuild" mode, returns only local data model view references.
160
- In "additive" mode, returns union of local and CDF data model view references.
161
-
162
- Returns:
163
- Set of ViewReference objects representing all data model views to validate.
164
- """
165
- return (
166
- self.local_resources.data_model_views.union(self.cdf_resources.data_model_views)
167
- if self.modus_operandi == "additive"
168
- else self.local_resources.data_model_views
169
- )
170
-
171
- @cached_property
172
- def views_references(self) -> set[ViewReference]:
173
- """Get all view references to validate based on deployment mode.
174
-
175
- In "rebuild" mode, returns only local view references.
176
- In "additive" mode, returns union of local and CDF view references.
177
-
178
- Returns:
179
- Set of ViewReference objects representing all views to validate.
180
- """
181
-
182
- return (
183
- set(self.local_resources.views_by_reference.keys()).union(set(self.cdf_resources.views_by_reference.keys()))
184
- if self.modus_operandi == "additive"
185
- else set(self.local_resources.views_by_reference.keys())
186
- )
187
-
188
- @cached_property
189
- def container_references(self) -> set[ContainerReference]:
190
- """Get all container references to validate based on deployment mode.
191
-
192
- In "rebuild" mode, returns only local container references.
193
- In "additive" mode, returns union of local and CDF container references.
194
-
195
- Returns:
196
- Set of ContainerReference objects representing all containers to validate.
197
- """
198
-
199
- return (
200
- set(self.local_resources.containers_by_reference.keys()).union(
201
- set(self.cdf_resources.containers_by_reference.keys())
202
- )
203
- if self.modus_operandi == "additive"
204
- else set(self.local_resources.containers_by_reference.keys())
205
- )
206
-
207
- @cached_property
208
- def merged_views(self) -> dict[ViewReference, ViewRequest]:
209
- """Get views with merged properties and implements for accurate limit checking.
210
-
211
- In "rebuild" mode, returns only local views.
212
- In "additive" mode, merges local and CDF views by:
213
- - Combining properties from both versions (local overrides CDF)
214
- - Combining implements lists (union, no duplicates)
215
- - Using CDF view as base if it exists, otherwise local view
216
-
217
- This ensures limit validation accounts for the actual deployed state
218
- in additive deployments.
219
-
220
- Returns:
221
- Dictionary mapping ViewReference to merged ViewRequest objects.
222
-
223
- Raises:
224
- RuntimeError: If a referenced view is not found in either local or CDF resources.
225
- """
226
-
227
- merged_views: dict[ViewReference, ViewRequest] = {}
228
-
229
- # Merge local views, combining properties if view exists in both
230
- # if rebuild mode , self.view_references returns only local views ref
231
- for view_ref in self.views_references:
232
- cdf_view = self.cdf_resources.views_by_reference.get(view_ref)
233
- local_view = self.local_resources.views_by_reference.get(view_ref)
234
-
235
- if self.modus_operandi == "additive" and not cdf_view and not local_view:
236
- raise RuntimeError(f"View {view_ref!s} not found in either local or CDF resources. This is a bug!")
237
- elif self.modus_operandi == "rebuild" and not local_view:
238
- raise RuntimeError(f"View {view_ref!s} not found in local resources. This is a bug!")
239
-
240
- merged_views[view_ref] = cast(ViewRequest, (cdf_view or local_view)).model_copy(deep=True)
241
-
242
- # in additive mode, we start off with CDF view as base , which we will update with local
243
- if (
244
- self.modus_operandi == "additive"
245
- or cast(ViewRequest, local_view).space != self.local_resources.data_model_reference.space
246
- ):
247
- merged_views[view_ref] = cast(ViewRequest, (cdf_view or local_view)).model_copy(deep=True)
248
-
249
- # rebuild mode
250
- else:
251
- merged_views[view_ref] = cast(ViewRequest, local_view).model_copy(deep=True)
252
-
253
- # this will later update of local properties and implements
254
- if local_view and local_view.properties:
255
- if not merged_views[view_ref].properties:
256
- merged_views[view_ref].properties = local_view.properties
257
- else:
258
- merged_views[view_ref].properties.update(local_view.properties)
259
-
260
- # Special handling for properties which originates from implements
261
-
262
- if local_view and local_view.implements:
263
- if not merged_views[view_ref].implements:
264
- merged_views[view_ref].implements = local_view.implements
265
- else: # mypy is complaining here about possible None which is not possible due to the check above
266
- for impl in local_view.implements:
267
- if impl not in cast(list[ViewReference], merged_views[view_ref].implements):
268
- cast(list[ViewReference], merged_views[view_ref].implements).append(impl)
269
-
270
- # this handles an edge case where view is defined locally, implements another view, which is not
271
- # present locally, but only in CDF, this could be typically the case when we define a view which
272
- # implements a Core Data Model view, while not defining any of its own properties (not uncommon)
273
- # locally in for example Excel sheets
274
- for implement in local_view.implements:
275
- if cdf_implement_view := self.cdf_resources.views_by_reference.get(implement):
276
- # in rebuild mode, we skip CDF views from the same space as rebuild mode is supposed to
277
- # rebuild everything from local resources only, but we still need to merge in properties
278
- # from CDF implements from other spaces
279
- if (
280
- self.modus_operandi == "rebuild"
281
- and cdf_implement_view.space == self.local_resources.data_model_reference.space
282
- ):
283
- continue
284
-
285
- for prop_name, prop in cdf_implement_view.properties.items():
286
- if merged_views[view_ref].properties and prop_name not in merged_views[view_ref].properties:
287
- merged_views[view_ref].properties[prop_name] = prop
288
- elif not merged_views[view_ref].properties:
289
- merged_views[view_ref].properties = {prop_name: prop}
290
-
291
- return merged_views
292
-
293
- @cached_property
294
- def merged_containers(self) -> dict[ContainerReference, ContainerRequest]:
295
- """Get containers with merged properties for accurate limit checking.
296
-
297
- In "rebuild" mode, returns only local containers.
298
- In "additive" mode, merges local and CDF containers by:
299
- - Combining properties from both versions (local overrides CDF)
300
- - Using CDF container as base if it exists, otherwise local container
301
-
302
- This ensures limit validation accounts for the actual deployed state
303
- in additive deployments.
304
-
305
- Returns:
306
- Dictionary mapping ContainerReference to merged ContainerRequest objects.
307
-
308
- Raises:
309
- RuntimeError: If a referenced container is not found in either local or CDF resources.
310
- """
311
-
312
- merged_containers: dict[ContainerReference, ContainerRequest] = {}
313
- # Merge local containers, combining properties if container exists in both
314
- for container_ref in self.container_references:
315
- cdf_container = self.cdf_resources.containers_by_reference.get(container_ref)
316
- local_container = self.local_resources.containers_by_reference.get(container_ref)
317
-
318
- if self.modus_operandi == "additive" and not cdf_container and not local_container:
319
- raise RuntimeError(
320
- f"Container {container_ref!s} not found in either local or CDF resources. This is a bug!"
321
- )
322
- elif self.modus_operandi == "rebuild" and not local_container:
323
- raise RuntimeError(f"Container {container_ref!s} not found in local resources. This is a bug!")
324
-
325
- # in additive mode, we start off with CDF container as base , which we will update with local
326
- if (
327
- self.modus_operandi == "additive"
328
- or cast(ContainerRequest, local_container).space != self.local_resources.data_model_reference.space
329
- ):
330
- merged_containers[container_ref] = cast(
331
- ContainerRequest, (cdf_container or local_container)
332
- ).model_copy(deep=True)
333
-
334
- else: # rebuild mode
335
- merged_containers[container_ref] = cast(ContainerRequest, local_container).model_copy(deep=True)
336
-
337
- if local_container and local_container.properties:
338
- if not merged_containers[container_ref].properties:
339
- merged_containers[container_ref].properties = local_container.properties
340
- else:
341
- merged_containers[container_ref].properties.update(local_container.properties)
342
-
343
- return merged_containers
344
-
345
- def _is_ancestor_of_target(self, potential_ancestor: ViewReference, target_view_ref: ViewReference) -> bool:
346
- """Check if a view is an ancestor of the target view."""
347
- return potential_ancestor in self.local_resources.ancestors_by_view_reference.get(
348
- target_view_ref, set()
349
- ) or potential_ancestor in self.cdf_resources.ancestors_by_view_reference.get(target_view_ref, set())