cognite-neat 0.109.4__py3-none-any.whl → 0.111.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.

Potentially problematic release.


This version of cognite-neat might be problematic. Click here for more details.

Files changed (88) hide show
  1. cognite/neat/_alpha.py +8 -0
  2. cognite/neat/_client/_api/schema.py +43 -1
  3. cognite/neat/_client/data_classes/schema.py +4 -4
  4. cognite/neat/_constants.py +15 -1
  5. cognite/neat/_graph/extractors/__init__.py +4 -0
  6. cognite/neat/_graph/extractors/_classic_cdf/_base.py +8 -16
  7. cognite/neat/_graph/extractors/_classic_cdf/_classic.py +48 -19
  8. cognite/neat/_graph/extractors/_classic_cdf/_relationships.py +23 -17
  9. cognite/neat/_graph/extractors/_classic_cdf/_sequences.py +15 -17
  10. cognite/neat/_graph/extractors/_dict.py +102 -0
  11. cognite/neat/_graph/extractors/_dms.py +27 -40
  12. cognite/neat/_graph/extractors/_dms_graph.py +30 -3
  13. cognite/neat/_graph/extractors/_iodd.py +3 -3
  14. cognite/neat/_graph/extractors/_mock_graph_generator.py +9 -7
  15. cognite/neat/_graph/extractors/_raw.py +67 -0
  16. cognite/neat/_graph/loaders/_base.py +20 -4
  17. cognite/neat/_graph/loaders/_rdf2dms.py +476 -383
  18. cognite/neat/_graph/queries/_base.py +163 -133
  19. cognite/neat/_graph/transformers/__init__.py +1 -3
  20. cognite/neat/_graph/transformers/_classic_cdf.py +6 -22
  21. cognite/neat/_graph/transformers/_rdfpath.py +2 -49
  22. cognite/neat/_issues/__init__.py +1 -6
  23. cognite/neat/_issues/_base.py +21 -252
  24. cognite/neat/_issues/_contextmanagers.py +46 -0
  25. cognite/neat/_issues/_factory.py +69 -0
  26. cognite/neat/_issues/errors/__init__.py +20 -4
  27. cognite/neat/_issues/errors/_external.py +7 -0
  28. cognite/neat/_issues/errors/_wrapper.py +81 -3
  29. cognite/neat/_issues/formatters.py +4 -4
  30. cognite/neat/_issues/warnings/__init__.py +3 -2
  31. cognite/neat/_issues/warnings/_properties.py +8 -0
  32. cognite/neat/_issues/warnings/user_modeling.py +12 -0
  33. cognite/neat/_rules/_constants.py +12 -0
  34. cognite/neat/_rules/_shared.py +3 -2
  35. cognite/neat/_rules/analysis/__init__.py +2 -3
  36. cognite/neat/_rules/analysis/_base.py +430 -259
  37. cognite/neat/_rules/catalog/info-rules-imf.xlsx +0 -0
  38. cognite/neat/_rules/exporters/_rules2excel.py +3 -9
  39. cognite/neat/_rules/exporters/_rules2instance_template.py +2 -2
  40. cognite/neat/_rules/exporters/_rules2ontology.py +5 -4
  41. cognite/neat/_rules/importers/_base.py +2 -47
  42. cognite/neat/_rules/importers/_dms2rules.py +7 -10
  43. cognite/neat/_rules/importers/_dtdl2rules/dtdl_importer.py +2 -2
  44. cognite/neat/_rules/importers/_rdf/_inference2rules.py +66 -26
  45. cognite/neat/_rules/importers/_rdf/_shared.py +1 -1
  46. cognite/neat/_rules/importers/_spreadsheet2rules.py +12 -9
  47. cognite/neat/_rules/models/_base_rules.py +0 -2
  48. cognite/neat/_rules/models/data_types.py +7 -0
  49. cognite/neat/_rules/models/dms/_exporter.py +9 -8
  50. cognite/neat/_rules/models/dms/_rules.py +29 -2
  51. cognite/neat/_rules/models/dms/_rules_input.py +9 -1
  52. cognite/neat/_rules/models/dms/_validation.py +115 -5
  53. cognite/neat/_rules/models/entities/_loaders.py +1 -1
  54. cognite/neat/_rules/models/entities/_multi_value.py +2 -2
  55. cognite/neat/_rules/models/entities/_single_value.py +8 -3
  56. cognite/neat/_rules/models/entities/_wrapped.py +2 -2
  57. cognite/neat/_rules/models/information/_rules.py +18 -17
  58. cognite/neat/_rules/models/information/_rules_input.py +3 -1
  59. cognite/neat/_rules/models/information/_validation.py +66 -17
  60. cognite/neat/_rules/transformers/__init__.py +8 -2
  61. cognite/neat/_rules/transformers/_converters.py +234 -44
  62. cognite/neat/_rules/transformers/_verification.py +5 -10
  63. cognite/neat/_session/_base.py +6 -4
  64. cognite/neat/_session/_explore.py +39 -0
  65. cognite/neat/_session/_inspect.py +25 -6
  66. cognite/neat/_session/_prepare.py +12 -0
  67. cognite/neat/_session/_read.py +88 -20
  68. cognite/neat/_session/_set.py +7 -1
  69. cognite/neat/_session/_show.py +11 -123
  70. cognite/neat/_session/_state.py +6 -2
  71. cognite/neat/_session/_subset.py +64 -0
  72. cognite/neat/_session/_to.py +177 -19
  73. cognite/neat/_store/_graph_store.py +9 -246
  74. cognite/neat/_utils/rdf_.py +36 -5
  75. cognite/neat/_utils/spreadsheet.py +44 -1
  76. cognite/neat/_utils/text.py +124 -37
  77. cognite/neat/_utils/upload.py +2 -0
  78. cognite/neat/_version.py +2 -2
  79. {cognite_neat-0.109.4.dist-info → cognite_neat-0.111.0.dist-info}/METADATA +1 -1
  80. {cognite_neat-0.109.4.dist-info → cognite_neat-0.111.0.dist-info}/RECORD +83 -82
  81. {cognite_neat-0.109.4.dist-info → cognite_neat-0.111.0.dist-info}/WHEEL +1 -1
  82. cognite/neat/_graph/queries/_construct.py +0 -187
  83. cognite/neat/_graph/queries/_shared.py +0 -173
  84. cognite/neat/_rules/analysis/_dms.py +0 -57
  85. cognite/neat/_rules/analysis/_information.py +0 -249
  86. cognite/neat/_rules/models/_rdfpath.py +0 -372
  87. {cognite_neat-0.109.4.dist-info → cognite_neat-0.111.0.dist-info}/LICENSE +0 -0
  88. {cognite_neat-0.109.4.dist-info → cognite_neat-0.111.0.dist-info}/entry_points.txt +0 -0
@@ -6,11 +6,12 @@ from collections import Counter, defaultdict
6
6
  from collections.abc import Collection, Mapping
7
7
  from datetime import date, datetime
8
8
  from functools import cached_property
9
- from typing import ClassVar, Literal, TypeVar, cast, overload
9
+ from typing import Any, ClassVar, Literal, TypeVar, cast, overload
10
10
 
11
11
  from cognite.client.data_classes import data_modeling as dms
12
12
  from cognite.client.data_classes.data_modeling import DataModelId, DataModelIdentifier, ViewId
13
13
  from cognite.client.utils.useful_types import SequenceNotStr
14
+ from pydantic import ValidationError
14
15
  from rdflib import Namespace
15
16
 
16
17
  from cognite.neat._client import NeatClient
@@ -33,7 +34,7 @@ from cognite.neat._rules._shared import (
33
34
  ReadRules,
34
35
  VerifiedRules,
35
36
  )
36
- from cognite.neat._rules.analysis import DMSAnalysis
37
+ from cognite.neat._rules.analysis import RulesAnalysis
37
38
  from cognite.neat._rules.importers import DMSImporter
38
39
  from cognite.neat._rules.models import (
39
40
  DMSInputRules,
@@ -43,8 +44,6 @@ from cognite.neat._rules.models import (
43
44
  SheetList,
44
45
  data_types,
45
46
  )
46
- from cognite.neat._rules.models._rdfpath import Entity as RDFPathEntity
47
- from cognite.neat._rules.models._rdfpath import RDFPath, SingleProperty
48
47
  from cognite.neat._rules.models.data_types import AnyURI, DataType, Enum, File, String, Timeseries
49
48
  from cognite.neat._rules.models.dms import DMSMetadata, DMSProperty, DMSValidation, DMSView
50
49
  from cognite.neat._rules.models.dms._rules import DMSContainer, DMSEnum, DMSNode
@@ -60,7 +59,8 @@ from cognite.neat._rules.models.entities import (
60
59
  ViewEntity,
61
60
  )
62
61
  from cognite.neat._rules.models.information import InformationClass, InformationMetadata, InformationProperty
63
- from cognite.neat._utils.text import NamingStandardization, to_camel
62
+ from cognite.neat._utils.rdf_ import get_inheritance_path
63
+ from cognite.neat._utils.text import NamingStandardization, title, to_camel_case, to_words
64
64
 
65
65
  from ._base import RulesTransformer, T_VerifiedIn, T_VerifiedOut, VerifiedRulesTransformer
66
66
  from ._verification import VerifyDMSRules
@@ -75,22 +75,20 @@ class ConversionTransformer(VerifiedRulesTransformer[T_VerifiedIn, T_VerifiedOut
75
75
  ...
76
76
 
77
77
 
78
- class ToInformationCompliantEntities(
79
- RulesTransformer[ReadRules[InformationInputRules], ReadRules[InformationInputRules]]
80
- ):
78
+ class ToDMSCompliantEntities(RulesTransformer[ReadRules[InformationInputRules], ReadRules[InformationInputRules]]):
81
79
  """Converts input rules to rules that is compliant with the Information Model.
82
80
 
83
81
  This is typically used with importers from arbitrary sources to ensure that classes and properties have valid
84
82
  names.
85
83
 
86
84
  Args:
87
- renaming: How to handle renaming of entities that are not compliant with the Information Model.
88
- - "warning": Raises a warning and renames the entity.
85
+ rename_warning: How to handle renaming of entities that are not compliant with the Information Model.
86
+ - "raise": Raises a warning and renames the entity.
89
87
  - "skip": Renames the entity without raising a warning.
90
88
  """
91
89
 
92
- def __init__(self, renaming: Literal["warning", "skip"] = "skip") -> None:
93
- self._renaming = renaming
90
+ def __init__(self, rename_warning: Literal["raise", "skip"] = "skip") -> None:
91
+ self._renaming = rename_warning
94
92
 
95
93
  @property
96
94
  def description(self) -> str:
@@ -107,9 +105,9 @@ class ToInformationCompliantEntities(
107
105
  new_by_old_class_suffix: dict[str, str] = {}
108
106
  for cls in copy.classes:
109
107
  cls_entity = cast(ClassEntity, cls.class_) # Safe due to the dump above
110
- if not PATTERNS.class_id_compliance.match(cls_entity.suffix):
108
+ if not PATTERNS.view_id_compliance.match(cls_entity.suffix):
111
109
  new_suffix = self._fix_cls_suffix(cls_entity.suffix)
112
- if self._renaming == "warning":
110
+ if self._renaming == "raise":
113
111
  warnings.warn(
114
112
  NeatValueWarning(f"Invalid class name {cls_entity.suffix!r}.Renaming to {new_suffix}"),
115
113
  stacklevel=2,
@@ -123,7 +121,7 @@ class ToInformationCompliantEntities(
123
121
  cls_.implements[i].suffix = new_by_old_class_suffix[parent.suffix] # type: ignore[union-attr]
124
122
 
125
123
  for prop in copy.properties:
126
- if not PATTERNS.information_property_id_compliance.match(prop.property_):
124
+ if not PATTERNS.dms_property_id_compliance.match(prop.property_):
127
125
  new_property = self._fix_property(prop.property_)
128
126
  if self._renaming == "warning":
129
127
  warnings.warn(
@@ -174,6 +172,65 @@ class ToInformationCompliantEntities(
174
172
  return property_
175
173
 
176
174
 
175
+ class StandardizeSpaceAndVersion(VerifiedRulesTransformer[DMSRules, DMSRules]): # type: ignore[misc]
176
+ """This transformer standardizes the space and version of the DMSRules.
177
+
178
+ typically used to ensure all the views are moved to the same version as the data model.
179
+
180
+ """
181
+
182
+ @property
183
+ def description(self) -> str:
184
+ return "Ensures uniform version and space of the views belonging to the data model."
185
+
186
+ def transform(self, rules: DMSRules) -> DMSRules:
187
+ copy = rules.model_copy(deep=True)
188
+
189
+ space = copy.metadata.space
190
+ version = copy.metadata.version
191
+
192
+ copy.views = self._standardize_views(copy.views, space, version)
193
+ copy.properties = self._standardize_properties(copy.properties, space, version)
194
+ return copy
195
+
196
+ def _standardize_views(self, views: SheetList[DMSView], space: str, version: str) -> SheetList[DMSView]:
197
+ for view in views:
198
+ if view.view.space not in COGNITE_SPACES:
199
+ view.view.version = version
200
+ view.view.prefix = space
201
+
202
+ if view.implements:
203
+ for i, parent in enumerate(view.implements):
204
+ if parent.space not in COGNITE_SPACES:
205
+ view.implements[i].version = version
206
+ view.implements[i].prefix = space
207
+ return views
208
+
209
+ def _standardize_properties(
210
+ self, properties: SheetList[DMSProperty], space: str, version: str
211
+ ) -> SheetList[DMSProperty]:
212
+ for property_ in properties:
213
+ if property_.view.space not in COGNITE_SPACES:
214
+ property_.view.version = version
215
+ property_.view.prefix = space
216
+
217
+ if isinstance(property_.value_type, ViewEntity) and property_.value_type.space not in COGNITE_SPACES:
218
+ property_.value_type.version = version
219
+ property_.value_type.prefix = space
220
+
221
+ # for edge connection
222
+ if (
223
+ property_.connection
224
+ and isinstance(property_.connection, EdgeEntity)
225
+ and property_.connection.properties
226
+ ):
227
+ if property_.connection.properties.space not in COGNITE_SPACES:
228
+ property_.connection.properties.version = version
229
+ property_.connection.properties.prefix = space
230
+
231
+ return properties
232
+
233
+
177
234
  class ToCompliantEntities(VerifiedRulesTransformer[InformationRules, InformationRules]): # type: ignore[misc]
178
235
  """Converts input rules to rules with compliant entity IDs that match regex patters used
179
236
  by DMS schema components."""
@@ -504,8 +561,9 @@ _T_Entity = TypeVar("_T_Entity", bound=ClassEntity | ViewEntity)
504
561
 
505
562
 
506
563
  class SetIDDMSModel(VerifiedRulesTransformer[DMSRules, DMSRules]):
507
- def __init__(self, new_id: DataModelId | tuple[str, str, str]):
564
+ def __init__(self, new_id: DataModelId | tuple[str, str, str], name: str | None = None):
508
565
  self.new_id = DataModelId.load(new_id)
566
+ self.name = name
509
567
 
510
568
  @property
511
569
  def description(self) -> str:
@@ -518,10 +576,14 @@ class SetIDDMSModel(VerifiedRulesTransformer[DMSRules, DMSRules]):
518
576
  dump["metadata"]["space"] = self.new_id.space
519
577
  dump["metadata"]["external_id"] = self.new_id.external_id
520
578
  dump["metadata"]["version"] = self.new_id.version
579
+ dump["metadata"]["name"] = self.name or self._generate_name()
521
580
  # Serialize and deserialize to set the new space and external_id
522
581
  # as the default values for the new model.
523
582
  return DMSRules.model_validate(DMSInputRules.load(dump).dump())
524
583
 
584
+ def _generate_name(self) -> str:
585
+ return title(to_words(self.new_id.external_id))
586
+
525
587
 
526
588
  class ToExtensionModel(VerifiedRulesTransformer[DMSRules, DMSRules], ABC):
527
589
  type_: ClassVar[str]
@@ -649,7 +711,7 @@ class ToEnterpriseModel(ToExtensionModel):
649
711
 
650
712
  container = DMSContainer(container=container_entity)
651
713
 
652
- property_id = f"{to_camel(view_entity.suffix)}{self.dummy_property}"
714
+ property_id = f"{to_camel_case(view_entity.suffix)}{self.dummy_property}"
653
715
  property_ = DMSProperty(
654
716
  view=view_entity,
655
717
  view_property=property_id,
@@ -759,13 +821,16 @@ class ToSolutionModel(ToExtensionModel):
759
821
 
760
822
  @staticmethod
761
823
  def _expand_properties(rules: DMSRules) -> DMSRules:
762
- probe = DMSAnalysis(rules)
763
- ancestor_properties_by_view = probe.classes_with_properties(
764
- consider_inheritance=True, allow_different_namespace=True
824
+ probe = RulesAnalysis(dms=rules)
825
+ ancestor_properties_by_view = probe.properties_by_view(
826
+ include_ancestors=True,
827
+ include_different_space=True,
765
828
  )
766
829
  property_ids_by_view = {
767
830
  view: {prop.view_property for prop in properties}
768
- for view, properties in probe.classes_with_properties(consider_inheritance=False).items()
831
+ for view, properties in probe.properties_by_view(
832
+ include_ancestors=False, include_different_space=True
833
+ ).items()
769
834
  }
770
835
  for view, property_ids in property_ids_by_view.items():
771
836
  ancestor_properties = ancestor_properties_by_view.get(view, [])
@@ -852,7 +917,7 @@ class ToSolutionModel(ToExtensionModel):
852
917
  if view.view in read_view_by_new_view:
853
918
  read_view = read_view_by_new_view[view.view]
854
919
  container_entity = ContainerEntity(space=self.new_model_id.space, externalId=view.view.external_id)
855
- prefix = to_camel(view.view.suffix)
920
+ prefix = to_camel_case(view.view.suffix)
856
921
  if self.properties == "repeat" and self.dummy_property:
857
922
  property_ = DMSProperty(
858
923
  view=view.view,
@@ -937,7 +1002,7 @@ class ToDataProductModel(ToSolutionModel):
937
1002
  self.include = include
938
1003
 
939
1004
  def transform(self, rules: DMSRules) -> DMSRules:
940
- # Overwrite this to avoid the warning.
1005
+ # Overwrite transform to avoid the warning.
941
1006
  return self._to_solution(rules)
942
1007
 
943
1008
 
@@ -1011,7 +1076,7 @@ class DropModelViews(VerifiedRulesTransformer[DMSRules, DMSRules]):
1011
1076
  }
1012
1077
  new_model = rules.model_copy(deep=True)
1013
1078
 
1014
- properties_by_view = DMSAnalysis(new_model).classes_with_properties(consider_inheritance=True)
1079
+ properties_by_view = RulesAnalysis(dms=new_model).properties_by_view(include_ancestors=True)
1015
1080
 
1016
1081
  new_model.views = SheetList[DMSView]([view for view in new_model.views if view.view not in exclude_views])
1017
1082
  new_properties = SheetList[DMSProperty]()
@@ -1155,6 +1220,7 @@ class ClassicPrepareCore(VerifiedRulesTransformer[InformationRules, InformationR
1155
1220
  class_=ClassEntity(prefix=prefix, suffix="ClassicSourceSystem"),
1156
1221
  description="A source system that provides data to the data model.",
1157
1222
  neatId=namespace["ClassicSourceSystem"],
1223
+ instance_source=self.instance_namespace["ClassicSourceSystem"],
1158
1224
  )
1159
1225
  output.classes.append(source_system_class)
1160
1226
  for prop in output.properties:
@@ -1183,15 +1249,7 @@ class ClassicPrepareCore(VerifiedRulesTransformer[InformationRules, InformationR
1183
1249
  value_type=String(),
1184
1250
  class_=ClassEntity(prefix=prefix, suffix="ClassicSourceSystem"),
1185
1251
  max_count=1,
1186
- instance_source=RDFPath(
1187
- traversal=SingleProperty(
1188
- class_=RDFPathEntity(
1189
- prefix=instance_prefix,
1190
- suffix="ClassicSourceSystem",
1191
- ),
1192
- property=RDFPathEntity(prefix=instance_prefix, suffix="name"),
1193
- ),
1194
- ),
1252
+ instance_source=[self.instance_namespace["name"]],
1195
1253
  )
1196
1254
  )
1197
1255
  return output
@@ -1673,7 +1731,6 @@ class _DMSRulesConverter:
1673
1731
  classes.append(info_class)
1674
1732
 
1675
1733
  prefixes = get_default_prefixes_and_namespaces()
1676
- instance_prefix: str | None = None
1677
1734
  if self.instance_namespace:
1678
1735
  instance_prefix = next((k for k, v in prefixes.items() if v == self.instance_namespace), None)
1679
1736
  if instance_prefix is None:
@@ -1696,15 +1753,6 @@ class _DMSRulesConverter:
1696
1753
  else:
1697
1754
  raise ValueError(f"Unsupported value type: {property_.value_type.type_}")
1698
1755
 
1699
- transformation: RDFPath | None = None
1700
- if instance_prefix is not None:
1701
- transformation = RDFPath(
1702
- traversal=SingleProperty(
1703
- class_=RDFPathEntity(prefix=instance_prefix, suffix=property_.view.external_id),
1704
- property=RDFPathEntity(prefix=instance_prefix, suffix=property_.view_property),
1705
- )
1706
- )
1707
-
1708
1756
  info_property = InformationProperty(
1709
1757
  # Removing version
1710
1758
  class_=ClassEntity(suffix=property_.view.suffix, prefix=property_.view.prefix),
@@ -1713,7 +1761,6 @@ class _DMSRulesConverter:
1713
1761
  description=property_.description,
1714
1762
  min_count=(0 if property_.nullable or property_.nullable is None else 1),
1715
1763
  max_count=(float("inf") if property_.is_list or property_.nullable is None else 1),
1716
- instance_source=transformation,
1717
1764
  )
1718
1765
 
1719
1766
  # Linking
@@ -1746,3 +1793,146 @@ class _DMSRulesConverter:
1746
1793
  created=metadata.created,
1747
1794
  updated=metadata.updated,
1748
1795
  )
1796
+
1797
+
1798
+ class SubsetDMSRules(VerifiedRulesTransformer[DMSRules, DMSRules]):
1799
+ """Subsets DMSRules to only include the specified views."""
1800
+
1801
+ def __init__(self, views: set[ViewEntity]):
1802
+ self._views = views
1803
+
1804
+ def transform(self, rules: DMSRules) -> DMSRules:
1805
+ analysis = RulesAnalysis(dms=rules)
1806
+
1807
+ views_by_view = analysis.view_by_view_entity
1808
+ implements_by_view = analysis.implements_by_view()
1809
+
1810
+ available = analysis.defined_views(include_ancestors=True)
1811
+ subset = available.intersection(self._views)
1812
+
1813
+ ancestors: set[ViewEntity] = set()
1814
+ for view in subset:
1815
+ ancestors = ancestors.union({ancestor for ancestor in get_inheritance_path(view, implements_by_view)})
1816
+ subset = subset.union(ancestors)
1817
+
1818
+ if not subset:
1819
+ raise NeatValueError("None of the requested views are defined in the rules!")
1820
+
1821
+ if nonexisting := self._views - subset:
1822
+ raise NeatValueError(
1823
+ "Following requested views do not exist"
1824
+ f" in the rules: [{','.join([view.external_id for view in nonexisting])}]. Aborting."
1825
+ )
1826
+
1827
+ subsetted_rules: dict[str, Any] = {
1828
+ "metadata": rules.metadata.model_copy(),
1829
+ "views": SheetList[DMSView](),
1830
+ "properties": SheetList[DMSProperty](),
1831
+ "containers": SheetList[DMSContainer](),
1832
+ "enum": rules.enum,
1833
+ "nodes": rules.nodes,
1834
+ }
1835
+
1836
+ # add views
1837
+ for view in subset:
1838
+ subsetted_rules["views"].append(views_by_view[view])
1839
+
1840
+ used_containers = set()
1841
+
1842
+ # add properties
1843
+ for view, properties in analysis.properties_by_view(include_ancestors=False).items():
1844
+ if view not in subset:
1845
+ continue
1846
+
1847
+ for property_ in properties:
1848
+ if (
1849
+ isinstance(property_.value_type, DataType)
1850
+ or isinstance(property_.value_type, DMSUnknownEntity)
1851
+ or (isinstance(property_.value_type, ViewEntity) and property_.value_type in subset)
1852
+ ):
1853
+ subsetted_rules["properties"].append(property_)
1854
+
1855
+ if property_.container:
1856
+ used_containers.add(property_.container)
1857
+
1858
+ # add containers
1859
+ if rules.containers:
1860
+ for container in rules.containers:
1861
+ if container.container in used_containers:
1862
+ subsetted_rules["containers"].append(container)
1863
+
1864
+ try:
1865
+ return DMSRules.model_validate(subsetted_rules)
1866
+ except ValidationError as e:
1867
+ raise NeatValueError(f"Cannot subset rules: {e}") from e
1868
+
1869
+
1870
+ class SubsetInformationRules(VerifiedRulesTransformer[InformationRules, InformationRules]):
1871
+ """Subsets InformationRules to only include the specified classes."""
1872
+
1873
+ def __init__(self, classes: set[ClassEntity]):
1874
+ self._classes = classes
1875
+
1876
+ def transform(self, rules: InformationRules) -> InformationRules:
1877
+ analysis = RulesAnalysis(information=rules)
1878
+
1879
+ class_by_class_entity = analysis.class_by_class_entity
1880
+ parent_entity_by_class_entity = analysis.parents_by_class()
1881
+
1882
+ available = analysis.defined_classes(include_ancestors=True)
1883
+ subset = available.intersection(self._classes)
1884
+
1885
+ # need to add all the parent classes of the desired classes to the possible classes
1886
+ ancestors: set[ClassEntity] = set()
1887
+ for class_ in subset:
1888
+ ancestors = ancestors.union(
1889
+ {ancestor for ancestor in get_inheritance_path(class_, parent_entity_by_class_entity)}
1890
+ )
1891
+ subset = subset.union(ancestors)
1892
+
1893
+ if not subset:
1894
+ raise NeatValueError("None of the requested classes are defined in the rules!")
1895
+
1896
+ if nonexisting := self._classes - subset:
1897
+ raise NeatValueError(
1898
+ "Following requested classes do not exist"
1899
+ f" in the rules: [{','.join([class_.suffix for class_ in nonexisting])}]"
1900
+ ". Aborting."
1901
+ )
1902
+
1903
+ subsetted_rules: dict[str, Any] = {
1904
+ "metadata": rules.metadata.model_copy(),
1905
+ "prefixes": (rules.prefixes or {}).copy(),
1906
+ "classes": SheetList[InformationClass](),
1907
+ "properties": SheetList[InformationProperty](),
1908
+ }
1909
+
1910
+ for class_ in subset:
1911
+ subsetted_rules["classes"].append(class_by_class_entity[class_])
1912
+
1913
+ for class_, properties in analysis.properties_by_class(include_ancestors=False).items():
1914
+ if class_ not in subset:
1915
+ continue
1916
+ for property_ in properties:
1917
+ # datatype property can be added directly
1918
+ if (
1919
+ isinstance(property_.value_type, DataType)
1920
+ or (isinstance(property_.value_type, ClassEntity) and property_.value_type in subset)
1921
+ or isinstance(property_.value_type, UnknownEntity)
1922
+ ):
1923
+ subsetted_rules["properties"].append(property_)
1924
+ # object property can be added if the value type is in the subset
1925
+ elif isinstance(property_.value_type, MultiValueTypeInfo):
1926
+ allowed = [t for t in property_.value_type.types if t in subset or isinstance(t, DataType)]
1927
+ if allowed:
1928
+ subsetted_rules["properties"].append(
1929
+ property_.model_copy(
1930
+ deep=True,
1931
+ update={"value_type": MultiValueTypeInfo(types=allowed)},
1932
+ )
1933
+ )
1934
+
1935
+ try:
1936
+ return InformationRules.model_validate(subsetted_rules)
1937
+ except ValidationError as e:
1938
+ raise NeatValueError(f"Cannot subset rules: {e}") from e
@@ -35,27 +35,22 @@ class VerificationTransformer(RulesTransformer[T_ReadInputRules, T_VerifiedRules
35
35
  in_ = rules.rules
36
36
  if in_ is None:
37
37
  raise NeatValueError("Cannot verify rules. The reading of the rules failed.")
38
- error_args = rules.read_context
39
38
  verified_rules: T_VerifiedRules | None = None
40
39
  # We need to catch issues as we use the error args to provide extra context for the errors/warnings
41
- # For example, which row in the spreadsheet the error occurred o
42
- with catch_issues(error_args=error_args) as issues:
40
+ # For example, which row in the spreadsheet the error occurred.
41
+ with catch_issues(rules.read_context) as issues:
43
42
  rules_cls = self._get_rules_cls(rules)
44
43
  dumped = in_.dump()
45
44
  verified_rules = rules_cls.model_validate(dumped) # type: ignore[assignment]
46
45
  if self.validate:
47
46
  validation_cls = self._get_validation_cls(verified_rules) # type: ignore[arg-type]
48
47
  if issubclass(validation_cls, DMSValidation):
49
- validation_issues = DMSValidation(verified_rules, self._client).validate() # type: ignore[arg-type]
48
+ validation_issues = DMSValidation(verified_rules, self._client, rules.read_context).validate() # type: ignore[arg-type]
50
49
  elif issubclass(validation_cls, InformationValidation):
51
- validation_issues = InformationValidation(verified_rules).validate() # type: ignore[arg-type]
50
+ validation_issues = InformationValidation(verified_rules, rules.read_context).validate() # type: ignore[arg-type]
52
51
  else:
53
52
  raise NeatValueError("Unsupported rule type")
54
-
55
- # Need to trigger and raise such that the catch_issues can add the extra context
56
- validation_issues.trigger_warnings()
57
- if validation_issues.has_errors:
58
- raise MultiValueError(validation_issues.errors)
53
+ issues.extend(validation_issues)
59
54
 
60
55
  # Raise issues which is expected to be handled outside of this method
61
56
  issues.trigger_warnings()
@@ -16,7 +16,7 @@ from cognite.neat._rules.transformers import (
16
16
  InformationToDMS,
17
17
  MergeDMSRules,
18
18
  MergeInformationRules,
19
- ToInformationCompliantEntities,
19
+ ToDMSCompliantEntities,
20
20
  VerifyInformationRules,
21
21
  )
22
22
  from cognite.neat._store._rules_store import RulesEntity
@@ -25,6 +25,7 @@ from cognite.neat._utils.auxiliary import local_import
25
25
  from ._collector import _COLLECTOR, Collector
26
26
  from ._create import CreateAPI
27
27
  from ._drop import DropAPI
28
+ from ._explore import ExploreAPI
28
29
  from ._fix import FixAPI
29
30
  from ._inspect import InspectAPI
30
31
  from ._mapping import MappingAPI
@@ -33,6 +34,7 @@ from ._read import ReadAPI
33
34
  from ._set import SetAPI
34
35
  from ._show import ShowAPI
35
36
  from ._state import SessionState
37
+ from ._subset import SubsetAPI
36
38
  from ._to import ToAPI
37
39
  from .engine import load_neat_engine
38
40
  from .exceptions import session_class_wrapper
@@ -101,7 +103,9 @@ class NeatSession:
101
103
  self.inspect = InspectAPI(self._state)
102
104
  self.mapping = MappingAPI(self._state)
103
105
  self.drop = DropAPI(self._state)
106
+ self.subset = SubsetAPI(self._state)
104
107
  self.create = CreateAPI(self._state)
108
+ self._explore = ExploreAPI(self._state)
105
109
  self.opt = OptAPI()
106
110
  self.opt._display()
107
111
  if load_engine != "skip" and (engine_version := load_neat_engine(client, load_engine)):
@@ -236,9 +240,7 @@ class NeatSession:
236
240
 
237
241
  def action() -> tuple[InformationRules, DMSRules | None]:
238
242
  unverified_information = importer.to_rules()
239
- unverified_information = ToInformationCompliantEntities(renaming="warning").transform(
240
- unverified_information
241
- )
243
+ unverified_information = ToDMSCompliantEntities(rename_warning="raise").transform(unverified_information)
242
244
 
243
245
  extra_info = VerifyInformationRules().transform(unverified_information)
244
246
  if not last_entity:
@@ -0,0 +1,39 @@
1
+ from typing import cast
2
+
3
+ import pandas as pd
4
+ from rdflib import URIRef
5
+
6
+ from cognite.neat._utils.rdf_ import remove_namespace_from_uri
7
+ from cognite.neat._utils.text import humanize_collection
8
+
9
+ from ._state import SessionState
10
+ from .exceptions import NeatSessionError, session_class_wrapper
11
+
12
+
13
+ @session_class_wrapper
14
+ class ExploreAPI:
15
+ """
16
+ Explore the instances in the session.
17
+ """
18
+
19
+ def __init__(self, state: SessionState):
20
+ self._state = state
21
+
22
+ def types(self) -> pd.DataFrame:
23
+ """List all the types of instances in the session."""
24
+ return pd.DataFrame(self._state.instances.store.queries.types_with_instance_and_property_count())
25
+
26
+ def properties(self) -> pd.DataFrame:
27
+ """List all the properties of a type of instances in the session."""
28
+ return pd.DataFrame(self._state.instances.store.queries.properties_with_count())
29
+
30
+ def instance_with_properties(self, type: str) -> dict[str, set[str]]:
31
+ """List all the instances of a type with their properties."""
32
+ available_types = self._state.instances.store.queries.list_types(remove_namespace=False)
33
+ uri_by_type = {remove_namespace_from_uri(t[0]): t[0] for t in available_types}
34
+ if type not in uri_by_type:
35
+ raise NeatSessionError(
36
+ f"Type {type} not found. Available types are: {humanize_collection(uri_by_type.keys())}"
37
+ )
38
+ type_uri = cast(URIRef, uri_by_type[type])
39
+ return self._state.instances.store.queries.instances_with_properties(type_uri, remove_namespace=True)
@@ -1,5 +1,5 @@
1
1
  import difflib
2
- from collections.abc import Callable
2
+ from collections.abc import Callable, Set
3
3
  from typing import Literal, overload
4
4
 
5
5
  import pandas as pd
@@ -85,11 +85,13 @@ class InspectIssues:
85
85
 
86
86
  def __init__(self, state: SessionState) -> None:
87
87
  self._state = state
88
+ self._max_display = 50
88
89
 
89
90
  @overload
90
91
  def __call__(
91
92
  self,
92
93
  search: str | None = None,
94
+ include: Literal["all", "errors", "warning"] | Set[Literal["all", "errors", "warning"]] = "all",
93
95
  return_dataframe: Literal[True] = (False if IN_NOTEBOOK else True), # type: ignore[assignment]
94
96
  ) -> pd.DataFrame: ...
95
97
 
@@ -97,12 +99,14 @@ class InspectIssues:
97
99
  def __call__(
98
100
  self,
99
101
  search: str | None = None,
102
+ include: Literal["all", "errors", "warning"] | Set[Literal["all", "errors", "warning"]] = "all",
100
103
  return_dataframe: Literal[False] = (False if IN_NOTEBOOK else True), # type: ignore[assignment]
101
104
  ) -> None: ...
102
105
 
103
106
  def __call__(
104
107
  self,
105
108
  search: str | None = None,
109
+ include: Literal["all", "errors", "warning"] | Set[Literal["all", "errors", "warning"]] = "all",
106
110
  return_dataframe: bool = (False if IN_NOTEBOOK else True), # type: ignore[assignment]
107
111
  ) -> pd.DataFrame | None:
108
112
  """Returns the issues of the current data model."""
@@ -113,6 +117,13 @@ class InspectIssues:
113
117
  elif issues is None:
114
118
  self._print("No issues found.")
115
119
  return pd.DataFrame() if return_dataframe else None
120
+ include_set = {include} if isinstance(include, str) else include
121
+ if "all" in include_set:
122
+ include_set = {"errors", "warning"}
123
+ if "warning" not in include_set:
124
+ issues = issues.errors
125
+ if "errors" not in include_set:
126
+ issues = issues.warnings
116
127
 
117
128
  if issues and search is not None:
118
129
  unique_types = {type(issue).__name__ for issue in issues}
@@ -120,18 +131,21 @@ class InspectIssues:
120
131
  issues = IssueList([issue for issue in issues if type(issue).__name__ in closest_match])
121
132
 
122
133
  issue_str = "\n".join(
123
- [f" * **{type(issue).__name__}**: {issue.as_message(include_type=False)}" for issue in issues]
134
+ [
135
+ f" * **{type(issue).__name__}**: {issue.as_message(include_type=False)}"
136
+ for issue in issues[: self._max_display]
137
+ ]
138
+ + ([] if len(issues) <= 50 else [f" * ... {len(issues) - self._max_display} more"])
124
139
  )
125
140
  markdown_str = f"### {len(issues)} issues found\n\n{issue_str}"
126
-
127
141
  if IN_NOTEBOOK:
128
142
  from IPython.display import Markdown, display
129
143
 
130
144
  display(Markdown(markdown_str))
131
145
  elif RICH_AVAILABLE:
132
- from rich import print
146
+ from rich import print as rprint
133
147
 
134
- print(RichMarkdown(markdown_str))
148
+ rprint(RichMarkdown(markdown_str))
135
149
 
136
150
  if return_dataframe:
137
151
  return issues.to_pandas()
@@ -170,6 +184,7 @@ class InspectOutcome:
170
184
  class InspectUploadOutcome:
171
185
  def __init__(self, get_last_outcome: Callable[[], UploadResultList]) -> None:
172
186
  self._get_last_outcome = get_last_outcome
187
+ self._max_display = 50
173
188
 
174
189
  @staticmethod
175
190
  def _as_set(value: str | list[str] | None) -> set[str] | None:
@@ -223,7 +238,7 @@ class InspectUploadOutcome:
223
238
  from IPython.display import Markdown, display
224
239
 
225
240
  lines: list[str] = []
226
- for item in outcome:
241
+ for line_no, item in enumerate(outcome):
227
242
  lines.append(f"### {item.name}")
228
243
  if unique_errors := set(item.error_messages):
229
244
  lines.append("#### Errors")
@@ -255,6 +270,10 @@ class InspectUploadOutcome:
255
270
  else:
256
271
  lines.append(f" * {value}")
257
272
 
273
+ if line_no >= self._max_display:
274
+ lines.append(f"### ... {len(outcome) - self._max_display} more")
275
+ break
276
+
258
277
  display(Markdown("\n".join(lines)))
259
278
 
260
279
  if return_dataframe: