cognite-neat 0.75.9__py3-none-any.whl → 0.76.1__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.
@@ -15,7 +15,7 @@ from cognite.neat.rules.models.entities import (
15
15
  ViewPropertyEntity,
16
16
  )
17
17
 
18
- from ._base import ExtensionCategory, SchemaCompleteness
18
+ from ._base import DataModelType, ExtensionCategory, SchemaCompleteness
19
19
  from ._dms_architect_rules import DMSContainer, DMSMetadata, DMSProperty, DMSRules, DMSView
20
20
 
21
21
 
@@ -27,11 +27,11 @@ class DMSMetadataWrite:
27
27
  creator: str
28
28
  version: str
29
29
  extension: Literal["addition", "reshape", "rebuild"] = "addition"
30
+ data_model_type: Literal["solution", "enterprise"] = "solution"
30
31
  name: str | None = None
31
32
  description: str | None = None
32
33
  created: datetime | str | None = None
33
34
  updated: datetime | str | None = None
34
- default_view_version: str | None = None
35
35
 
36
36
  @classmethod
37
37
  def load(cls, data: dict[str, Any] | None) -> "DMSMetadataWrite | None":
@@ -45,11 +45,11 @@ class DMSMetadataWrite:
45
45
  creator=data.get("creator"), # type: ignore[arg-type]
46
46
  version=data.get("version"), # type: ignore[arg-type]
47
47
  extension=data.get("extension", "addition"),
48
+ data_model_type=data.get("data_model_type", "solution"),
48
49
  name=data.get("name"),
49
50
  description=data.get("description"),
50
51
  created=data.get("created"),
51
52
  updated=data.get("updated"),
52
- default_view_version=data.get("default_view_version"),
53
53
  )
54
54
 
55
55
  def dump(self) -> dict[str, Any]:
@@ -58,13 +58,13 @@ class DMSMetadataWrite:
58
58
  extension=ExtensionCategory(self.extension),
59
59
  space=self.space,
60
60
  externalId=self.external_id,
61
+ dataModelType=DataModelType(self.data_model_type),
61
62
  creator=self.creator,
62
63
  version=self.version,
63
64
  name=self.name,
64
65
  description=self.description,
65
66
  created=self.created or datetime.now(),
66
67
  updated=self.updated or datetime.now(),
67
- default_view_version=self.default_view_version or self.version,
68
68
  )
69
69
 
70
70
 
@@ -77,7 +77,7 @@ class DMSPropertyWrite:
77
77
  class_: str | None = None
78
78
  name: str | None = None
79
79
  description: str | None = None
80
- relation: Literal["direct", "reversedirect", "multiedge"] | None = None
80
+ connection: Literal["direct", "edge", "reverse"] | None = None
81
81
  nullable: bool | None = None
82
82
  is_list: bool | None = None
83
83
  default: str | int | dict | None = None
@@ -118,7 +118,7 @@ class DMSPropertyWrite:
118
118
  class_=data.get("class_"),
119
119
  name=data.get("name"),
120
120
  description=data.get("description"),
121
- relation=data.get("relation"),
121
+ connection=data.get("connection"),
122
122
  nullable=data.get("nullable"),
123
123
  is_list=data.get("is_list"),
124
124
  default=data.get("default"),
@@ -143,23 +143,25 @@ class DMSPropertyWrite:
143
143
 
144
144
  return {
145
145
  "View": ViewEntity.load(self.view, space=default_space, version=default_version),
146
- "ViewProperty": self.view_property,
146
+ "View Property": self.view_property,
147
147
  "Value Type": value_type,
148
- "Property": self.property_ or self.view_property,
149
- "Class": ClassEntity.load(self.class_, prefix=default_space, version=default_version)
150
- if self.class_
151
- else None,
148
+ "Property (linage)": self.property_ or self.view_property,
149
+ "Class (linage)": (
150
+ ClassEntity.load(self.class_, prefix=default_space, version=default_version) if self.class_ else None
151
+ ),
152
152
  "Name": self.name,
153
153
  "Description": self.description,
154
- "Relation": self.relation,
154
+ "Connection": self.connection,
155
155
  "Nullable": self.nullable,
156
- "IsList": self.is_list,
156
+ "Is List": self.is_list,
157
157
  "Default": self.default,
158
158
  "Reference": self.reference,
159
- "Container": ContainerEntity.load(self.container, space=default_space, version=default_version)
160
- if self.container
161
- else None,
162
- "ContainerProperty": self.container_property,
159
+ "Container": (
160
+ ContainerEntity.load(self.container, space=default_space, version=default_version)
161
+ if self.container
162
+ else None
163
+ ),
164
+ "Container Property": self.container_property,
163
165
  "Index": self.index,
164
166
  "Constraint": self.constraint,
165
167
  }
@@ -208,19 +210,23 @@ class DMSContainerWrite:
208
210
 
209
211
  def dump(self, default_space: str) -> dict[str, Any]:
210
212
  container = ContainerEntity.load(self.container, space=default_space)
211
- return dict(
212
- Container=container,
213
- Class=ClassEntity.load(self.class_, prefix=default_space) if self.class_ else container.as_class(),
214
- Name=self.name,
215
- Description=self.description,
216
- Reference=self.reference,
217
- Constraint=[
218
- ContainerEntity.load(constraint.strip(), space=default_space)
219
- for constraint in self.constraint.split(",")
220
- ]
221
- if self.constraint
222
- else None,
223
- )
213
+ return {
214
+ "Container": container,
215
+ "Class (linage)": (
216
+ ClassEntity.load(self.class_, prefix=default_space) if self.class_ else container.as_class()
217
+ ),
218
+ "Name": self.name,
219
+ "Description": self.description,
220
+ "Reference": self.reference,
221
+ "Constraint": (
222
+ [
223
+ ContainerEntity.load(constraint.strip(), space=default_space)
224
+ for constraint in self.constraint.split(",")
225
+ ]
226
+ if self.constraint
227
+ else None
228
+ ),
229
+ }
224
230
 
225
231
 
226
232
  @dataclass
@@ -268,23 +274,27 @@ class DMSViewWrite:
268
274
 
269
275
  def dump(self, default_space: str, default_version: str) -> dict[str, Any]:
270
276
  view = ViewEntity.load(self.view, space=default_space, version=default_version)
271
- return dict(
272
- View=view,
273
- Class=ClassEntity.load(self.class_, prefix=default_space, version=default_version)
274
- if self.class_
275
- else view.as_class(),
276
- Name=self.name,
277
- Description=self.description,
278
- Implements=[
279
- ViewEntity.load(implement, space=default_space, version=default_version)
280
- for implement in self.implements.split(",")
281
- ]
282
- if self.implements
283
- else None,
284
- Reference=self.reference,
285
- Filter=self.filter_,
286
- InModel=self.in_model,
287
- )
277
+ return {
278
+ "View": view,
279
+ "Class (linage)": (
280
+ ClassEntity.load(self.class_, prefix=default_space, version=default_version)
281
+ if self.class_
282
+ else view.as_class()
283
+ ),
284
+ "Name": self.name,
285
+ "Description": self.description,
286
+ "Implements": (
287
+ [
288
+ ViewEntity.load(implement, space=default_space, version=default_version)
289
+ for implement in self.implements.split(",")
290
+ ]
291
+ if self.implements
292
+ else None
293
+ ),
294
+ "Reference": self.reference,
295
+ "Filter": self.filter_,
296
+ "In Model": self.in_model,
297
+ }
288
298
 
289
299
 
290
300
  @dataclass
@@ -3,7 +3,7 @@ import sys
3
3
  import warnings
4
4
  import zipfile
5
5
  from collections import Counter, defaultdict
6
- from dataclasses import dataclass, field, fields
6
+ from dataclasses import Field, dataclass, field, fields
7
7
  from pathlib import Path
8
8
  from typing import Any, ClassVar, cast
9
9
 
@@ -14,6 +14,7 @@ from cognite.client.data_classes import DatabaseWrite, DatabaseWriteList, Transf
14
14
  from cognite.client.data_classes.data_modeling import ViewApply
15
15
  from cognite.client.data_classes.transformations.common import Edges, EdgeType, Nodes, ViewInfo
16
16
 
17
+ from cognite.neat.rules import issues
17
18
  from cognite.neat.rules.issues.dms import (
18
19
  ContainerPropertyUsedMultipleTimesError,
19
20
  DirectRelationMissingSourceWarning,
@@ -106,23 +107,33 @@ class DMSSchema:
106
107
  The directory is expected to follow the Cognite-Toolkit convention
107
108
  where each file is named as `resource_type.resource_name.yaml`.
108
109
  """
109
- data = cls._read_directory(Path(directory))
110
- return cls.load(data)
110
+ data, context = cls._read_directory(Path(directory))
111
+ return cls.load(data, context)
111
112
 
112
113
  @classmethod
113
- def _read_directory(cls, directory: Path) -> dict[str, list[Any]]:
114
+ def _read_directory(cls, directory: Path) -> tuple[dict[str, list[Any]], dict[str, list[Path]]]:
114
115
  data: dict[str, Any] = {}
116
+ context: dict[str, list[Path]] = {}
115
117
  for yaml_file in directory.rglob("*.yaml"):
116
118
  if "." in yaml_file.stem:
117
119
  resource_type = yaml_file.stem.rsplit(".", 1)[-1]
118
120
  if attr_name := cls._FIELD_NAME_BY_RESOURCE_TYPE.get(resource_type):
119
121
  data.setdefault(attr_name, [])
120
- loaded = yaml.safe_load(yaml_file.read_text())
122
+ context.setdefault(attr_name, [])
123
+ try:
124
+ # Using CSafeLoader over safe_load for ~10x speedup
125
+ loaded = yaml.safe_load(yaml_file.read_text())
126
+ except Exception as e:
127
+ warnings.warn(issues.fileread.InvalidFileFormatWarning(yaml_file, str(e)), stacklevel=2)
128
+ continue
129
+
121
130
  if isinstance(loaded, list):
122
131
  data[attr_name].extend(loaded)
132
+ context[attr_name].extend([yaml_file] * len(loaded))
123
133
  else:
124
134
  data[attr_name].append(loaded)
125
- return data
135
+ context[attr_name].append(yaml_file)
136
+ return data, context
126
137
 
127
138
  def to_directory(
128
139
  self,
@@ -182,12 +193,13 @@ class DMSSchema:
182
193
  The ZIP file is expected to follow the Cognite-Toolkit convention
183
194
  where each file is named as `resource_type.resource_name.yaml`.
184
195
  """
185
- data = cls._read_zip(Path(zip_file))
186
- return cls.load(data)
196
+ data, context = cls._read_zip(Path(zip_file))
197
+ return cls.load(data, context)
187
198
 
188
199
  @classmethod
189
- def _read_zip(cls, zip_file: Path) -> dict[str, list[Any]]:
200
+ def _read_zip(cls, zip_file: Path) -> tuple[dict[str, list[Any]], dict[str, list[Path]]]:
190
201
  data: dict[str, list[Any]] = {}
202
+ context: dict[str, list[Path]] = {}
191
203
  with zipfile.ZipFile(zip_file, "r") as zip_ref:
192
204
  for file_info in zip_ref.infolist():
193
205
  if file_info.filename.endswith(".yaml"):
@@ -199,12 +211,19 @@ class DMSSchema:
199
211
  resource_type = filename.stem.rsplit(".", 1)[-1]
200
212
  if attr_name := cls._FIELD_NAME_BY_RESOURCE_TYPE.get(resource_type):
201
213
  data.setdefault(attr_name, [])
202
- loaded = yaml.safe_load(zip_ref.read(file_info).decode())
214
+ context.setdefault(attr_name, [])
215
+ try:
216
+ loaded = yaml.safe_load(zip_ref.read(file_info).decode())
217
+ except Exception as e:
218
+ warnings.warn(issues.fileread.InvalidFileFormatWarning(filename, str(e)), stacklevel=2)
219
+ continue
203
220
  if isinstance(loaded, list):
204
221
  data[attr_name].extend(loaded)
222
+ context[attr_name].extend([filename] * len(loaded))
205
223
  else:
206
224
  data[attr_name].append(loaded)
207
- return data
225
+ context[attr_name].append(filename)
226
+ return data, context
208
227
 
209
228
  def to_zip(self, zip_file: str | Path, exclude: set[str] | None = None) -> None:
210
229
  """Save the schema to a ZIP file as YAML files. This is compatible with the Cognite-Toolkit convention.
@@ -234,18 +253,63 @@ class DMSSchema:
234
253
  zip_ref.writestr(f"data_models/nodes/{node.external_id}.node.yaml", node.dump_yaml())
235
254
 
236
255
  @classmethod
237
- def load(cls, data: str | dict[str, Any]) -> Self:
256
+ def load(cls, data: str | dict[str, list[Any]], context: dict[str, list[Path]] | None = None) -> Self:
257
+ """Loads a schema from a dictionary or a YAML or JSON formatted string.
258
+
259
+ Args:
260
+ data: The data to load the schema from. This can be a dictionary, a YAML or JSON formatted string.
261
+ context: This provides linage for where the data was loaded from. This is used in Warnings
262
+ if a single item fails to load.
263
+
264
+ Returns:
265
+ DMSSchema: The loaded schema.
266
+ """
267
+ context = context or {}
238
268
  if isinstance(data, str):
239
269
  # YAML is a superset of JSON, so we can use the same parser
240
- data_dict = yaml.safe_load(data)
270
+ try:
271
+ data_dict = yaml.safe_load(data)
272
+ except Exception as e:
273
+ raise issues.fileread.FailedStringLoadError(".yaml", str(e)).as_exception() from None
274
+ if not isinstance(data_dict, dict) and all(isinstance(v, list) for v in data_dict.values()):
275
+ raise issues.fileread.FailedStringLoadError(
276
+ "dict[str, list[Any]]", f"Invalid data structure: {type(data)}"
277
+ ).as_exception() from None
241
278
  else:
242
279
  data_dict = data
243
280
  loaded: dict[str, Any] = {}
244
281
  for attr in fields(cls):
245
282
  if items := data_dict.get(attr.name) or data_dict.get(to_camel(attr.name)):
246
- loaded[attr.name] = attr.type.load(items)
283
+ try:
284
+ loaded[attr.name] = attr.type.load(items)
285
+ except Exception as e:
286
+ loaded[attr.name] = cls._load_individual_resources(items, attr, str(e), context.get(attr.name, []))
247
287
  return cls(**loaded)
248
288
 
289
+ @classmethod
290
+ def _load_individual_resources(cls, items: list, attr: Field, trigger_error: str, resource_context) -> list[Any]:
291
+ resources = attr.type([])
292
+ if not hasattr(attr.type, "_RESOURCE"):
293
+ warnings.warn(
294
+ issues.fileread.FailedLoadWarning(Path("UNKNOWN"), attr.type.__name__, trigger_error), stacklevel=2
295
+ )
296
+ return resources
297
+ # Fallback to load individual resources.
298
+ single_cls = attr.type._RESOURCE
299
+ for no, item in enumerate(items):
300
+ try:
301
+ loaded_instance = single_cls.load(item)
302
+ except Exception as e:
303
+ try:
304
+ filepath = resource_context[no]
305
+ except IndexError:
306
+ filepath = Path("UNKNOWN")
307
+ # We use repr(e) instead of str(e) to include the exception type in the warning message
308
+ warnings.warn(issues.fileread.FailedLoadWarning(filepath, single_cls.__name__, repr(e)), stacklevel=2)
309
+ else:
310
+ resources.append(loaded_instance)
311
+ return resources
312
+
249
313
  def dump(self, camel_case: bool = True, sort: bool = True) -> dict[str, Any]:
250
314
  """Dump the schema to a dictionary that can be serialized to JSON.
251
315
 
@@ -410,6 +474,18 @@ class DMSSchema:
410
474
  )
411
475
  return None
412
476
 
477
+ def referenced_spaces(self) -> set[str]:
478
+ referenced_spaces = {container.space for container in self.containers}
479
+ referenced_spaces |= {view.space for view in self.views}
480
+ referenced_spaces |= {container.space for view in self.views for container in view.referenced_containers()}
481
+ referenced_spaces |= {parent.space for view in self.views for parent in view.implements or []}
482
+ referenced_spaces |= {node.space for node in self.node_types}
483
+ referenced_spaces |= {model.space for model in self.data_models}
484
+ referenced_spaces |= {view.space for model in self.data_models for view in model.views or []}
485
+ referenced_spaces |= {s.space for s in self.spaces}
486
+
487
+ return referenced_spaces
488
+
413
489
 
414
490
  @dataclass
415
491
  class PipelineSchema(DMSSchema):
@@ -429,18 +505,25 @@ class PipelineSchema(DMSSchema):
429
505
  self.databases.extend([DatabaseWrite(name=database) for database in missing])
430
506
 
431
507
  @classmethod
432
- def _read_directory(cls, directory: Path) -> dict[str, list[Any]]:
433
- data = super()._read_directory(directory)
508
+ def _read_directory(cls, directory: Path) -> tuple[dict[str, list[Any]], dict[str, list[Path]]]:
509
+ data, context = super()._read_directory(directory)
434
510
  for yaml_file in directory.rglob("*.yaml"):
435
511
  if yaml_file.parent.name in ("transformations", "raw"):
436
512
  attr_name = cls._FIELD_NAME_BY_RESOURCE_TYPE.get(yaml_file.parent.name, yaml_file.parent.name)
437
513
  data.setdefault(attr_name, [])
438
- loaded = yaml.safe_load(yaml_file.read_text())
514
+ context.setdefault(attr_name, [])
515
+ try:
516
+ loaded = yaml.safe_load(yaml_file.read_text())
517
+ except Exception as e:
518
+ warnings.warn(issues.fileread.InvalidFileFormatWarning(yaml_file, str(e)), stacklevel=2)
519
+ continue
439
520
  if isinstance(loaded, list):
440
521
  data[attr_name].extend(loaded)
522
+ context[attr_name].extend([yaml_file] * len(loaded))
441
523
  else:
442
524
  data[attr_name].append(loaded)
443
- return data
525
+ context[attr_name].append(yaml_file)
526
+ return data, context
444
527
 
445
528
  def to_directory(
446
529
  self,
@@ -483,8 +566,8 @@ class PipelineSchema(DMSSchema):
483
566
  zip_ref.writestr(f"raw/{raw_table.name}.yaml", raw_table.dump_yaml())
484
567
 
485
568
  @classmethod
486
- def _read_zip(cls, zip_file: Path) -> dict[str, list[Any]]:
487
- data = super()._read_zip(zip_file)
569
+ def _read_zip(cls, zip_file: Path) -> tuple[dict[str, list[Any]], dict[str, list[Path]]]:
570
+ data, context = super()._read_zip(zip_file)
488
571
  with zipfile.ZipFile(zip_file, "r") as zip_ref:
489
572
  for file_info in zip_ref.infolist():
490
573
  if file_info.filename.endswith(".yaml"):
@@ -494,12 +577,19 @@ class PipelineSchema(DMSSchema):
494
577
  if (parent := filepath.parent.name) in ("transformations", "raw"):
495
578
  attr_name = cls._FIELD_NAME_BY_RESOURCE_TYPE.get(parent, parent)
496
579
  data.setdefault(attr_name, [])
497
- loaded = yaml.safe_load(zip_ref.read(file_info).decode())
580
+ context.setdefault(attr_name, [])
581
+ try:
582
+ loaded = yaml.safe_load(zip_ref.read(file_info).decode())
583
+ except Exception as e:
584
+ warnings.warn(issues.fileread.InvalidFileFormatWarning(filepath, str(e)), stacklevel=2)
585
+ continue
498
586
  if isinstance(loaded, list):
499
587
  data[attr_name].extend(loaded)
588
+ context[attr_name].extend([filepath] * len(loaded))
500
589
  else:
501
590
  data[attr_name].append(loaded)
502
- return data
591
+ context[attr_name].append(filepath)
592
+ return data, context
503
593
 
504
594
  @classmethod
505
595
  def from_dms(cls, schema: DMSSchema, instance_space: str | None = None) -> "PipelineSchema":
@@ -23,7 +23,10 @@ class DomainMetadata(BaseMetadata):
23
23
 
24
24
 
25
25
  class DomainProperty(SheetEntity):
26
+ class_: ClassEntity = Field(alias="Class")
26
27
  property_: PropertyType = Field(alias="Property")
28
+ name: str | None = Field(alias="Name", default=None)
29
+ description: str | None = Field(alias="Description", default=None)
27
30
  value_type: DataType | ClassEntity = Field(alias="Value Type")
28
31
  min_count: int | None = Field(alias="Min Count", default=None)
29
32
  max_count: int | float | None = Field(alias="Max Count", default=None)
@@ -42,6 +45,8 @@ class DomainProperty(SheetEntity):
42
45
 
43
46
 
44
47
  class DomainClass(SheetEntity):
48
+ class_: ClassEntity = Field(alias="Class")
49
+ name: str | None = Field(alias="Name", default=None)
45
50
  description: str | None = Field(None, alias="Description")
46
51
  parent: ParentEntityList | None = Field(alias="Parent Class")
47
52
 
@@ -45,6 +45,7 @@ from cognite.neat.rules.models.rdfpath import (
45
45
 
46
46
  from ._base import (
47
47
  BaseMetadata,
48
+ DataModelType,
48
49
  ExtensionCategory,
49
50
  ExtensionCategoryType,
50
51
  MatchType,
@@ -75,6 +76,7 @@ else:
75
76
 
76
77
  class InformationMetadata(BaseMetadata):
77
78
  role: ClassVar[RoleTypes] = RoleTypes.information_architect
79
+ data_model_type: DataModelType = Field(DataModelType.solution, alias="dataModelType")
78
80
  schema_: SchemaCompleteness = Field(alias="schema")
79
81
  extension: ExtensionCategoryType | None = ExtensionCategory.addition
80
82
  prefix: PrefixType
@@ -123,6 +125,9 @@ class InformationClass(SheetEntity):
123
125
  match_type: The match type of the resource being described and the source entity.
124
126
  """
125
127
 
128
+ class_: ClassEntity = Field(alias="Class")
129
+ name: str | None = Field(alias="Name", default=None)
130
+ description: str | None = Field(alias="Description", default=None)
126
131
  parent: ParentEntityList | None = Field(alias="Parent Class", default=None)
127
132
  reference: URLEntity | ReferenceEntity | None = Field(alias="Reference", default=None, union_mode="left_to_right")
128
133
  match_type: MatchType | None = Field(alias="Match Type", default=None)
@@ -150,7 +155,10 @@ class InformationProperty(SheetEntity):
150
155
  knowledge graph. Defaults to None (no transformation)
151
156
  """
152
157
 
158
+ class_: ClassEntity = Field(alias="Class")
153
159
  property_: PropertyType = Field(alias="Property")
160
+ name: str | None = Field(alias="Name", default=None)
161
+ description: str | None = Field(alias="Description", default=None)
154
162
  value_type: DataType | ClassEntity | UnknownEntity = Field(alias="Value Type", union_mode="left_to_right")
155
163
  min_count: int | None = Field(alias="Min Count", default=None)
156
164
  max_count: int | float | None = Field(alias="Max Count", default=None)
@@ -430,7 +438,7 @@ class _InformationRulesConverter:
430
438
  for class_ in self.information.classes:
431
439
  properties: list[DMSProperty] = properties_by_class.get(class_.class_.versioned_id, [])
432
440
  if not properties or all(
433
- isinstance(prop.value_type, ViewPropertyEntity) and prop.relation != "direct" for prop in properties
441
+ isinstance(prop.value_type, ViewPropertyEntity) and prop.connection != "direct" for prop in properties
434
442
  ):
435
443
  classes_without_properties.add(class_.class_.versioned_id)
436
444
 
@@ -480,16 +488,15 @@ class _InformationRulesConverter:
480
488
  else:
481
489
  raise ValueError(f"Unsupported value type: {prop.value_type.type_}")
482
490
 
483
- relation: Literal["direct", "multiedge"] | None = None
491
+ relation: Literal["direct", "edge", "reverse"] | None = None
484
492
  if isinstance(value_type, ViewEntity | ViewPropertyEntity):
485
- relation = "multiedge" if prop.is_list else "direct"
493
+ relation = "edge" if prop.is_list else "direct"
486
494
 
487
495
  container: ContainerEntity | None = None
488
496
  container_property: str | None = None
489
497
  is_list: bool | None = prop.is_list
490
498
  nullable: bool | None = not prop.is_mandatory
491
- if relation == "multiedge":
492
- is_list = None
499
+ if relation == "edge":
493
500
  nullable = None
494
501
  elif relation == "direct":
495
502
  nullable = True
@@ -504,7 +511,7 @@ class _InformationRulesConverter:
504
511
  value_type=value_type,
505
512
  nullable=nullable,
506
513
  is_list=is_list,
507
- relation=relation,
514
+ connection=relation,
508
515
  default=prop.default,
509
516
  reference=prop.reference,
510
517
  container=container,