cognite-neat 0.87.0__py3-none-any.whl → 0.87.3__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.
- cognite/neat/_version.py +1 -1
- cognite/neat/app/api/configuration.py +1 -10
- cognite/neat/app/api/routers/data_exploration.py +1 -1
- cognite/neat/config.py +84 -17
- cognite/neat/graph/extractors/_classic_cdf/_assets.py +1 -1
- cognite/neat/graph/extractors/_classic_cdf/_events.py +1 -1
- cognite/neat/graph/extractors/_classic_cdf/_files.py +1 -1
- cognite/neat/graph/extractors/_classic_cdf/_labels.py +1 -1
- cognite/neat/graph/extractors/_classic_cdf/_relationships.py +1 -1
- cognite/neat/graph/extractors/_classic_cdf/_sequences.py +1 -1
- cognite/neat/graph/extractors/_classic_cdf/_timeseries.py +1 -1
- cognite/neat/graph/extractors/_dexpi.py +1 -1
- cognite/neat/graph/extractors/_mock_graph_generator.py +1 -1
- cognite/neat/graph/loaders/_rdf2asset.py +108 -52
- cognite/neat/graph/loaders/_rdf2dms.py +6 -6
- cognite/neat/graph/queries/_base.py +20 -11
- cognite/neat/graph/queries/_construct.py +3 -3
- cognite/neat/graph/queries/_shared.py +1 -1
- cognite/neat/graph/stores/_base.py +14 -3
- cognite/neat/graph/transformers/__init__.py +3 -0
- cognite/neat/graph/transformers/_rdfpath.py +42 -0
- cognite/neat/legacy/graph/extractors/_mock_graph_generator.py +1 -1
- cognite/neat/legacy/graph/loaders/_asset_loader.py +2 -2
- cognite/neat/legacy/graph/loaders/core/rdf_to_assets.py +5 -2
- cognite/neat/legacy/graph/loaders/core/rdf_to_relationships.py +4 -1
- cognite/neat/legacy/graph/loaders/rdf_to_dms.py +3 -1
- cognite/neat/legacy/graph/transformations/query_generator/sparql.py +1 -1
- cognite/neat/legacy/graph/transformations/transformer.py +1 -1
- cognite/neat/legacy/rules/exporters/_rules2dms.py +8 -3
- cognite/neat/legacy/rules/exporters/_rules2graphql.py +1 -1
- cognite/neat/legacy/rules/exporters/_rules2ontology.py +2 -1
- cognite/neat/legacy/rules/exporters/_rules2pydantic_models.py +3 -4
- cognite/neat/legacy/rules/importers/_dms2rules.py +4 -1
- cognite/neat/legacy/rules/importers/_graph2rules.py +5 -32
- cognite/neat/legacy/rules/importers/_owl2rules/_owl2classes.py +1 -1
- cognite/neat/legacy/rules/importers/_owl2rules/_owl2metadata.py +2 -1
- cognite/neat/legacy/rules/importers/_owl2rules/_owl2properties.py +1 -1
- cognite/neat/legacy/rules/models/raw_rules.py +1 -1
- cognite/neat/rules/analysis/_asset.py +15 -0
- cognite/neat/rules/analysis/_base.py +1 -1
- cognite/neat/rules/analysis/_information.py +40 -12
- cognite/neat/rules/exporters/_rules2dms.py +1 -1
- cognite/neat/rules/exporters/_rules2ontology.py +2 -1
- cognite/neat/rules/importers/_dms2rules.py +3 -1
- cognite/neat/rules/importers/_inference2rules.py +1 -5
- cognite/neat/rules/importers/_owl2rules/_owl2classes.py +1 -1
- cognite/neat/rules/importers/_owl2rules/_owl2metadata.py +2 -1
- cognite/neat/rules/importers/_owl2rules/_owl2properties.py +1 -1
- cognite/neat/rules/issues/spreadsheet.py +35 -0
- cognite/neat/rules/models/_rdfpath.py +17 -21
- cognite/neat/rules/models/asset/_validation.py +38 -1
- cognite/neat/rules/models/dms/_exporter.py +7 -3
- cognite/neat/rules/models/dms/_schema.py +5 -4
- cognite/neat/rules/models/entities.py +26 -8
- cognite/neat/rules/models/information/_validation.py +1 -1
- cognite/neat/utils/__init__.py +0 -3
- cognite/neat/utils/auth.py +47 -28
- cognite/neat/utils/auxiliary.py +141 -1
- cognite/neat/utils/cdf/__init__.py +0 -0
- cognite/neat/utils/{cdf_classes.py → cdf/data_classes.py} +122 -2
- cognite/neat/utils/{cdf_loaders → cdf/loaders}/_data_modeling.py +37 -0
- cognite/neat/utils/{cdf_loaders → cdf/loaders}/_ingestion.py +2 -1
- cognite/neat/utils/collection_.py +18 -0
- cognite/neat/utils/rdf_.py +165 -0
- cognite/neat/utils/text.py +4 -0
- cognite/neat/utils/time_.py +17 -0
- cognite/neat/utils/upload.py +13 -1
- cognite/neat/workflows/_exceptions.py +5 -5
- cognite/neat/workflows/base.py +1 -1
- cognite/neat/workflows/steps/lib/current/rules_validator.py +2 -2
- cognite/neat/workflows/steps/lib/legacy/graph_extractor.py +1 -1
- cognite/neat/workflows/steps/lib/legacy/graph_loader.py +1 -1
- cognite/neat/workflows/steps/lib/legacy/rules_exporter.py +1 -1
- cognite/neat/workflows/steps/lib/legacy/rules_importer.py +1 -1
- {cognite_neat-0.87.0.dist-info → cognite_neat-0.87.3.dist-info}/METADATA +2 -2
- {cognite_neat-0.87.0.dist-info → cognite_neat-0.87.3.dist-info}/RECORD +81 -81
- cognite/neat/utils/cdf.py +0 -59
- cognite/neat/utils/cdf_loaders/data_classes.py +0 -121
- cognite/neat/utils/exceptions.py +0 -41
- cognite/neat/utils/utils.py +0 -429
- /cognite/neat/utils/{cdf_loaders → cdf/loaders}/__init__.py +0 -0
- /cognite/neat/utils/{cdf_loaders → cdf/loaders}/_base.py +0 -0
- {cognite_neat-0.87.0.dist-info → cognite_neat-0.87.3.dist-info}/LICENSE +0 -0
- {cognite_neat-0.87.0.dist-info → cognite_neat-0.87.3.dist-info}/WHEEL +0 -0
- {cognite_neat-0.87.0.dist-info → cognite_neat-0.87.3.dist-info}/entry_points.txt +0 -0
|
@@ -295,6 +295,41 @@ class ClassNoPropertiesNoParentError(NeatValidationError):
|
|
|
295
295
|
return f"Class {self.classes[0]} have no direct or inherited properties. This may be a mistake."
|
|
296
296
|
|
|
297
297
|
|
|
298
|
+
@dataclass(frozen=True)
|
|
299
|
+
class AssetRulesHaveCircularDependencyError(NeatValidationError):
|
|
300
|
+
description = "Asset rules have circular dependencies."
|
|
301
|
+
fix = "Linking between classes via property that maps to parent_external_id must yield hierarchy structure."
|
|
302
|
+
|
|
303
|
+
classes: list[str]
|
|
304
|
+
|
|
305
|
+
def dump(self) -> dict[str, list[tuple[str, str]]]:
|
|
306
|
+
output = super().dump()
|
|
307
|
+
output["classes"] = self.classes
|
|
308
|
+
return output
|
|
309
|
+
|
|
310
|
+
def message(self) -> str:
|
|
311
|
+
return f"Asset rules have circular dependencies between classes {', '.join(self.classes)}."
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@dataclass(frozen=True)
|
|
315
|
+
class AssetParentPropertyPointsToDataValueTypeError(NeatValidationError):
|
|
316
|
+
description = "Parent property points to a data value type instead of a class."
|
|
317
|
+
fix = "Make sure that the parent property points to a class."
|
|
318
|
+
|
|
319
|
+
class_property_with_data_value_type: list[tuple[str, str]]
|
|
320
|
+
|
|
321
|
+
def dump(self) -> dict[str, list[tuple[str, str]]]:
|
|
322
|
+
output = super().dump()
|
|
323
|
+
output["class_property"] = self.class_property_with_data_value_type
|
|
324
|
+
return output
|
|
325
|
+
|
|
326
|
+
def message(self) -> str:
|
|
327
|
+
text = [
|
|
328
|
+
f"class {class_} property {property_}" for class_, property_ in self.class_property_with_data_value_type
|
|
329
|
+
]
|
|
330
|
+
return f"Following {', and'.join(text)} point to data value type instead to classes. This is a mistake."
|
|
331
|
+
|
|
332
|
+
|
|
298
333
|
@dataclass(frozen=True)
|
|
299
334
|
class ParentClassesNotDefinedError(NeatValidationError):
|
|
300
335
|
description = "Parent classes are not defined."
|
|
@@ -232,21 +232,12 @@ class SingleProperty(Traversal):
|
|
|
232
232
|
return f"{self.class_}({self.property})"
|
|
233
233
|
|
|
234
234
|
|
|
235
|
-
class
|
|
235
|
+
class SelfReferenceProperty(Traversal):
|
|
236
236
|
@classmethod
|
|
237
237
|
def from_string(cls, class_: str) -> Self:
|
|
238
238
|
return cls(class_=Entity.from_string(class_))
|
|
239
239
|
|
|
240
240
|
|
|
241
|
-
class AllProperties(Traversal):
|
|
242
|
-
@classmethod
|
|
243
|
-
def from_string(cls, class_: str) -> Self:
|
|
244
|
-
return cls(class_=Entity.from_string(class_))
|
|
245
|
-
|
|
246
|
-
def __str__(self) -> str:
|
|
247
|
-
return f"{self.class_}(*)"
|
|
248
|
-
|
|
249
|
-
|
|
250
241
|
class Origin(BaseModel):
|
|
251
242
|
class_: Entity
|
|
252
243
|
|
|
@@ -290,7 +281,7 @@ class Query(BaseModel):
|
|
|
290
281
|
|
|
291
282
|
|
|
292
283
|
class RDFPath(Rule):
|
|
293
|
-
traversal: SingleProperty |
|
|
284
|
+
traversal: SingleProperty | SelfReferenceProperty | Hop
|
|
294
285
|
|
|
295
286
|
def __str__(self) -> str:
|
|
296
287
|
return f"{self.traversal}"
|
|
@@ -311,14 +302,13 @@ class SPARQLQuery(RDFPath):
|
|
|
311
302
|
traversal: Query
|
|
312
303
|
|
|
313
304
|
|
|
314
|
-
def parse_traversal(raw: str) ->
|
|
305
|
+
def parse_traversal(raw: str) -> SelfReferenceProperty | SingleProperty | Hop:
|
|
315
306
|
if result := CLASS_ID_REGEX_COMPILED.match(raw):
|
|
316
|
-
return
|
|
317
|
-
elif result := ALL_PROPERTIES_REGEX_COMPILED.match(raw):
|
|
318
|
-
return AllProperties.from_string(class_=result.group(EntityTypes.class_))
|
|
307
|
+
return SelfReferenceProperty.from_string(class_=result.group(EntityTypes.class_))
|
|
319
308
|
elif result := SINGLE_PROPERTY_REGEX_COMPILED.match(raw):
|
|
320
309
|
return SingleProperty.from_string(
|
|
321
|
-
class_=result.group(EntityTypes.class_),
|
|
310
|
+
class_=result.group(EntityTypes.class_),
|
|
311
|
+
property_=result.group(EntityTypes.property_),
|
|
322
312
|
)
|
|
323
313
|
elif result := HOP_REGEX_COMPILED.match(raw):
|
|
324
314
|
return Hop.from_string(class_=result.group("origin"), traversal=result.group(_traversal))
|
|
@@ -329,7 +319,9 @@ def parse_traversal(raw: str) -> AllReferences | AllProperties | SingleProperty
|
|
|
329
319
|
def parse_table_lookup(raw: str) -> TableLookup:
|
|
330
320
|
if result := TABLE_REGEX_COMPILED.match(raw):
|
|
331
321
|
return TableLookup(
|
|
332
|
-
name=result.group(Lookup.table),
|
|
322
|
+
name=result.group(Lookup.table),
|
|
323
|
+
key=result.group(Lookup.key),
|
|
324
|
+
value=result.group(Lookup.value),
|
|
333
325
|
)
|
|
334
326
|
raise exceptions.NotValidTableLookUp(raw).to_pydantic_custom_error()
|
|
335
327
|
|
|
@@ -344,7 +336,10 @@ def parse_rule(rule_raw: str, rule_type: TransformationRuleType | None) -> RDFPa
|
|
|
344
336
|
if Counter(rule_raw).get("|") != 1:
|
|
345
337
|
raise exceptions.NotValidRAWLookUp(rule_raw).to_pydantic_custom_error()
|
|
346
338
|
traversal, table_lookup = rule_raw.split("|")
|
|
347
|
-
return RawLookup(
|
|
339
|
+
return RawLookup(
|
|
340
|
+
traversal=parse_traversal(traversal),
|
|
341
|
+
table=parse_table_lookup(table_lookup),
|
|
342
|
+
)
|
|
348
343
|
case TransformationRuleType.sparql:
|
|
349
344
|
return SPARQLQuery(traversal=Query(query=rule_raw))
|
|
350
345
|
case None:
|
|
@@ -352,9 +347,10 @@ def parse_rule(rule_raw: str, rule_type: TransformationRuleType | None) -> RDFPa
|
|
|
352
347
|
|
|
353
348
|
|
|
354
349
|
def is_valid_rule(rule_type: TransformationRuleType, rule_raw: str) -> bool:
|
|
355
|
-
is_valid_rule = {
|
|
356
|
-
|
|
357
|
-
|
|
350
|
+
is_valid_rule = {
|
|
351
|
+
TransformationRuleType.rdfpath: is_rdfpath,
|
|
352
|
+
TransformationRuleType.rawlookup: is_rawlookup,
|
|
353
|
+
}[rule_type]
|
|
358
354
|
return is_valid_rule(rule_raw)
|
|
359
355
|
|
|
360
356
|
|
|
@@ -1,4 +1,41 @@
|
|
|
1
|
+
from graphlib import CycleError
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
from cognite.neat.rules import issues
|
|
5
|
+
from cognite.neat.rules.issues.base import IssueList
|
|
6
|
+
from cognite.neat.rules.models._base import SheetList
|
|
7
|
+
from cognite.neat.rules.models.asset._rules import AssetProperty, AssetRules
|
|
8
|
+
from cognite.neat.rules.models.entities import AssetEntity, AssetFields, ClassEntity
|
|
1
9
|
from cognite.neat.rules.models.information._validation import InformationPostValidation
|
|
2
10
|
|
|
3
11
|
|
|
4
|
-
class AssetPostValidation(InformationPostValidation):
|
|
12
|
+
class AssetPostValidation(InformationPostValidation):
|
|
13
|
+
def validate(self) -> IssueList:
|
|
14
|
+
self.issue_list = super().validate()
|
|
15
|
+
self._parent_property_point_to_class()
|
|
16
|
+
self._circular_dependency()
|
|
17
|
+
return self.issue_list
|
|
18
|
+
|
|
19
|
+
def _parent_property_point_to_class(self) -> None:
|
|
20
|
+
class_property_with_data_value_type = []
|
|
21
|
+
for property_ in cast(SheetList[AssetProperty], self.properties):
|
|
22
|
+
for implementation in property_.implementation:
|
|
23
|
+
if (
|
|
24
|
+
isinstance(implementation, AssetEntity)
|
|
25
|
+
and implementation.property_ == AssetFields.parentExternalId
|
|
26
|
+
and not isinstance(property_.value_type, ClassEntity)
|
|
27
|
+
):
|
|
28
|
+
class_property_with_data_value_type.append((property_.class_.suffix, property_.property_))
|
|
29
|
+
|
|
30
|
+
if class_property_with_data_value_type:
|
|
31
|
+
self.issue_list.append(
|
|
32
|
+
issues.spreadsheet.AssetParentPropertyPointsToDataValueTypeError(class_property_with_data_value_type)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def _circular_dependency(self) -> None:
|
|
36
|
+
from cognite.neat.rules.analysis import AssetAnalysis
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
_ = AssetAnalysis(cast(AssetRules, self.rules)).class_topological_sort()
|
|
40
|
+
except CycleError as error:
|
|
41
|
+
self.issue_list.append(issues.spreadsheet.AssetRulesHaveCircularDependencyError(error.args[1]))
|
|
@@ -5,6 +5,7 @@ from typing import Any, cast
|
|
|
5
5
|
|
|
6
6
|
from cognite.client.data_classes import data_modeling as dm
|
|
7
7
|
from cognite.client.data_classes.data_modeling.containers import BTreeIndex
|
|
8
|
+
from cognite.client.data_classes.data_modeling.data_types import ListablePropertyType
|
|
8
9
|
from cognite.client.data_classes.data_modeling.views import (
|
|
9
10
|
SingleEdgeConnectionApply,
|
|
10
11
|
SingleReverseDirectRelationApply,
|
|
@@ -23,7 +24,7 @@ from cognite.neat.rules.models.entities import (
|
|
|
23
24
|
ViewPropertyEntity,
|
|
24
25
|
)
|
|
25
26
|
from cognite.neat.rules.models.wrapped_entities import DMSFilter, HasDataFilter, NodeTypeFilter
|
|
26
|
-
from cognite.neat.utils.
|
|
27
|
+
from cognite.neat.utils.cdf.data_classes import ContainerApplyDict, NodeApplyDict, SpaceApplyDict, ViewApplyDict
|
|
27
28
|
|
|
28
29
|
from ._rules import DMSMetadata, DMSProperty, DMSRules, DMSView
|
|
29
30
|
from ._schema import DMSSchema, PipelineSchema
|
|
@@ -282,8 +283,11 @@ class _DMSExporter:
|
|
|
282
283
|
type_cls = prop.value_type.dms
|
|
283
284
|
else:
|
|
284
285
|
type_cls = dm.DirectRelation
|
|
285
|
-
|
|
286
|
-
|
|
286
|
+
type_: dm.PropertyType
|
|
287
|
+
if issubclass(type_cls, ListablePropertyType):
|
|
288
|
+
type_ = type_cls(is_list=prop.is_list or False)
|
|
289
|
+
else:
|
|
290
|
+
type_ = type_cls()
|
|
287
291
|
container.properties[prop.container_property] = dm.ContainerProperty(
|
|
288
292
|
type=type_,
|
|
289
293
|
nullable=prop.nullable if prop.nullable is not None else True,
|
|
@@ -42,17 +42,18 @@ from cognite.neat.rules.issues.dms import (
|
|
|
42
42
|
MissingViewInModelWarning,
|
|
43
43
|
)
|
|
44
44
|
from cognite.neat.rules.models.data_types import _DATA_TYPE_BY_DMS_TYPE
|
|
45
|
-
from cognite.neat.utils.
|
|
45
|
+
from cognite.neat.utils.cdf.data_classes import (
|
|
46
46
|
CogniteResourceDict,
|
|
47
47
|
ContainerApplyDict,
|
|
48
48
|
NodeApplyDict,
|
|
49
|
+
RawTableWrite,
|
|
50
|
+
RawTableWriteList,
|
|
49
51
|
SpaceApplyDict,
|
|
50
52
|
ViewApplyDict,
|
|
51
53
|
)
|
|
52
|
-
from cognite.neat.utils.
|
|
53
|
-
from cognite.neat.utils.
|
|
54
|
+
from cognite.neat.utils.cdf.loaders import ViewLoader
|
|
55
|
+
from cognite.neat.utils.rdf_ import get_inheritance_path
|
|
54
56
|
from cognite.neat.utils.text import to_camel
|
|
55
|
-
from cognite.neat.utils.utils import get_inheritance_path
|
|
56
57
|
|
|
57
58
|
if sys.version_info >= (3, 11):
|
|
58
59
|
from typing import Self
|
|
@@ -4,7 +4,13 @@ from abc import ABC, abstractmethod
|
|
|
4
4
|
from functools import total_ordering
|
|
5
5
|
from typing import Annotated, Any, ClassVar, Generic, TypeVar, cast
|
|
6
6
|
|
|
7
|
-
from cognite.client.data_classes.data_modeling.ids import
|
|
7
|
+
from cognite.client.data_classes.data_modeling.ids import (
|
|
8
|
+
ContainerId,
|
|
9
|
+
DataModelId,
|
|
10
|
+
NodeId,
|
|
11
|
+
PropertyId,
|
|
12
|
+
ViewId,
|
|
13
|
+
)
|
|
8
14
|
from pydantic import (
|
|
9
15
|
AnyHttpUrl,
|
|
10
16
|
BaseModel,
|
|
@@ -16,7 +22,7 @@ from pydantic import (
|
|
|
16
22
|
)
|
|
17
23
|
|
|
18
24
|
from cognite.neat.rules.models.data_types import DataType
|
|
19
|
-
from cognite.neat.utils.
|
|
25
|
+
from cognite.neat.utils.text import replace_non_alphanumeric_with_underscore
|
|
20
26
|
|
|
21
27
|
if sys.version_info >= (3, 11):
|
|
22
28
|
from enum import StrEnum
|
|
@@ -263,9 +269,9 @@ class UnknownEntity(ClassEntity):
|
|
|
263
269
|
|
|
264
270
|
|
|
265
271
|
class AssetFields(StrEnum):
|
|
266
|
-
|
|
272
|
+
externalId = "externalId"
|
|
267
273
|
name = "name"
|
|
268
|
-
|
|
274
|
+
parentExternalId = "parentExternalId"
|
|
269
275
|
description = "description"
|
|
270
276
|
metadata = "metadata"
|
|
271
277
|
|
|
@@ -331,7 +337,8 @@ class MultiValueTypeInfo(BaseModel):
|
|
|
331
337
|
else:
|
|
332
338
|
return {
|
|
333
339
|
"types": [
|
|
334
|
-
DataType.load(type_) if DataType.is_data_type(type_) else ClassEntity.load(type_)
|
|
340
|
+
(DataType.load(type_) if DataType.is_data_type(type_) else ClassEntity.load(type_))
|
|
341
|
+
for type_ in types
|
|
335
342
|
]
|
|
336
343
|
}
|
|
337
344
|
|
|
@@ -447,7 +454,10 @@ class ViewPropertyEntity(DMSVersionedEntity[PropertyId]):
|
|
|
447
454
|
property_: str = Field(alias="property")
|
|
448
455
|
|
|
449
456
|
def as_id(self) -> PropertyId:
|
|
450
|
-
return PropertyId(
|
|
457
|
+
return PropertyId(
|
|
458
|
+
source=ViewId(self.space, self.external_id, self.version),
|
|
459
|
+
property=self.property_,
|
|
460
|
+
)
|
|
451
461
|
|
|
452
462
|
def as_view_id(self) -> ViewId:
|
|
453
463
|
return ViewId(space=self.space, external_id=self.external_id, version=self.version)
|
|
@@ -459,7 +469,10 @@ class ViewPropertyEntity(DMSVersionedEntity[PropertyId]):
|
|
|
459
469
|
if id.source.version is None:
|
|
460
470
|
raise ValueError("Version must be specified")
|
|
461
471
|
return cls(
|
|
462
|
-
space=id.source.space,
|
|
472
|
+
space=id.source.space,
|
|
473
|
+
externalId=id.source.external_id,
|
|
474
|
+
version=id.source.version,
|
|
475
|
+
property=id.property,
|
|
463
476
|
)
|
|
464
477
|
|
|
465
478
|
|
|
@@ -495,7 +508,12 @@ class ReferenceEntity(ClassEntity):
|
|
|
495
508
|
@classmethod
|
|
496
509
|
def from_entity(cls, entity: Entity, property_: str) -> "ReferenceEntity":
|
|
497
510
|
if isinstance(entity, ClassEntity):
|
|
498
|
-
return cls(
|
|
511
|
+
return cls(
|
|
512
|
+
prefix=str(entity.prefix),
|
|
513
|
+
suffix=entity.suffix,
|
|
514
|
+
version=entity.version,
|
|
515
|
+
property=property_,
|
|
516
|
+
)
|
|
499
517
|
else:
|
|
500
518
|
return cls(prefix=str(entity.prefix), suffix=entity.suffix, property=property_)
|
|
501
519
|
|
|
@@ -6,7 +6,7 @@ from cognite.neat.rules import issues
|
|
|
6
6
|
from cognite.neat.rules.issues import IssueList
|
|
7
7
|
from cognite.neat.rules.models._base import DataModelType, SchemaCompleteness
|
|
8
8
|
from cognite.neat.rules.models.entities import ClassEntity, EntityTypes, UnknownEntity
|
|
9
|
-
from cognite.neat.utils.
|
|
9
|
+
from cognite.neat.utils.rdf_ import get_inheritance_path
|
|
10
10
|
|
|
11
11
|
from ._rules import InformationRules
|
|
12
12
|
|
cognite/neat/utils/__init__.py
CHANGED
cognite/neat/utils/auth.py
CHANGED
|
@@ -5,13 +5,13 @@ from dataclasses import dataclass, fields
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Literal, TypeAlias, get_args
|
|
7
7
|
|
|
8
|
-
from cognite.client import CogniteClient
|
|
8
|
+
from cognite.client import ClientConfig, CogniteClient
|
|
9
9
|
from cognite.client.credentials import CredentialProvider, OAuthClientCredentials, OAuthInteractive, Token
|
|
10
10
|
|
|
11
11
|
from cognite.neat import _version
|
|
12
12
|
from cognite.neat.utils.auxiliary import local_import
|
|
13
13
|
|
|
14
|
-
__all__ = ["get_cognite_client"]
|
|
14
|
+
__all__ = ["get_cognite_client", "EnvironmentVariables"]
|
|
15
15
|
|
|
16
16
|
_LOGIN_FLOW: TypeAlias = Literal["infer", "client_credentials", "interactive", "token"]
|
|
17
17
|
_VALID_LOGIN_FLOWS = get_args(_LOGIN_FLOW)
|
|
@@ -22,7 +22,7 @@ def get_cognite_client(env_file_name: str = ".env") -> CogniteClient:
|
|
|
22
22
|
if not env_file_name.endswith(".env"):
|
|
23
23
|
raise ValueError("env_file_name must end with '.env'")
|
|
24
24
|
with suppress(KeyError):
|
|
25
|
-
variables =
|
|
25
|
+
variables = EnvironmentVariables.create_from_environ()
|
|
26
26
|
return variables.get_client()
|
|
27
27
|
|
|
28
28
|
repo_root = _repo_root()
|
|
@@ -51,7 +51,7 @@ def get_cognite_client(env_file_name: str = ".env") -> CogniteClient:
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
@dataclass
|
|
54
|
-
class
|
|
54
|
+
class EnvironmentVariables:
|
|
55
55
|
CDF_CLUSTER: str
|
|
56
56
|
CDF_PROJECT: str
|
|
57
57
|
LOGIN_FLOW: _LOGIN_FLOW = "infer"
|
|
@@ -66,10 +66,17 @@ class _EnvironmentVariables:
|
|
|
66
66
|
IDP_AUDIENCE: str | None = None
|
|
67
67
|
IDP_SCOPES: str | None = None
|
|
68
68
|
IDP_AUTHORITY_URL: str | None = None
|
|
69
|
+
CDF_MAX_WORKERS: int | None = None
|
|
70
|
+
CDF_TIMEOUT: int | None = None
|
|
71
|
+
CDF_REDIRECT_PORT: int = 53_000
|
|
69
72
|
|
|
70
73
|
def __post_init__(self):
|
|
71
74
|
if self.LOGIN_FLOW.lower() not in _VALID_LOGIN_FLOWS:
|
|
72
75
|
raise ValueError(f"LOGIN_FLOW must be one of {_VALID_LOGIN_FLOWS}")
|
|
76
|
+
if self.IDP_TOKEN_URL and not self.IDP_TENANT_ID:
|
|
77
|
+
prefix, suffix = "https://login.microsoftonline.com/", "/oauth2/v2.0/token"
|
|
78
|
+
if self.IDP_TOKEN_URL.startswith(prefix) and self.IDP_TOKEN_URL.endswith(suffix):
|
|
79
|
+
self.IDP_TENANT_ID = self.IDP_TOKEN_URL.removeprefix(prefix).removesuffix(suffix)
|
|
73
80
|
|
|
74
81
|
@property
|
|
75
82
|
def cdf_url(self) -> str:
|
|
@@ -102,7 +109,7 @@ class _EnvironmentVariables:
|
|
|
102
109
|
return f"https://login.microsoftonline.com/{self.IDP_TENANT_ID}"
|
|
103
110
|
|
|
104
111
|
@classmethod
|
|
105
|
-
def create_from_environ(cls) -> "
|
|
112
|
+
def create_from_environ(cls) -> "EnvironmentVariables":
|
|
106
113
|
if "CDF_CLUSTER" not in os.environ or "CDF_PROJECT" not in os.environ:
|
|
107
114
|
raise KeyError("CDF_CLUSTER and CDF_PROJECT must be set in the environment.", "CDF_CLUSTER", "CDF_PROJECT")
|
|
108
115
|
|
|
@@ -119,6 +126,25 @@ class _EnvironmentVariables:
|
|
|
119
126
|
IDP_AUDIENCE=os.environ.get("IDP_AUDIENCE"),
|
|
120
127
|
IDP_SCOPES=os.environ.get("IDP_SCOPES"),
|
|
121
128
|
IDP_AUTHORITY_URL=os.environ.get("IDP_AUTHORITY_URL"),
|
|
129
|
+
CDF_MAX_WORKERS=int(os.environ["CDF_MAX_WORKERS"]) if "CDF_MAX_WORKERS" in os.environ else None,
|
|
130
|
+
CDF_TIMEOUT=int(os.environ["CDF_TIMEOUT"]) if "CDF_TIMEOUT" in os.environ else None,
|
|
131
|
+
CDF_REDIRECT_PORT=int(os.environ.get("CDF_REDIRECT_PORT", 53_000)),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def default(cls) -> "EnvironmentVariables":
|
|
136
|
+
# This method is for backwards compatibility with the old config
|
|
137
|
+
# It is not recommended to use this method.
|
|
138
|
+
return cls(
|
|
139
|
+
LOGIN_FLOW="client_credentials",
|
|
140
|
+
CDF_CLUSTER="api",
|
|
141
|
+
CDF_PROJECT="dev",
|
|
142
|
+
IDP_TENANT_ID="common",
|
|
143
|
+
IDP_CLIENT_ID="neat",
|
|
144
|
+
IDP_CLIENT_SECRET="secret",
|
|
145
|
+
IDP_SCOPES="project:read,project:write",
|
|
146
|
+
CDF_TIMEOUT=60,
|
|
147
|
+
CDF_MAX_WORKERS=3,
|
|
122
148
|
)
|
|
123
149
|
|
|
124
150
|
def get_credentials(self) -> CredentialProvider:
|
|
@@ -161,7 +187,7 @@ class _EnvironmentVariables:
|
|
|
161
187
|
return OAuthInteractive(
|
|
162
188
|
client_id=self.IDP_CLIENT_ID,
|
|
163
189
|
authority_url=self.idp_authority_url,
|
|
164
|
-
redirect_port=
|
|
190
|
+
redirect_port=self.CDF_REDIRECT_PORT,
|
|
165
191
|
scopes=self.idp_scopes,
|
|
166
192
|
)
|
|
167
193
|
|
|
@@ -171,9 +197,15 @@ class _EnvironmentVariables:
|
|
|
171
197
|
return Token(self.TOKEN)
|
|
172
198
|
|
|
173
199
|
def get_client(self) -> CogniteClient:
|
|
174
|
-
|
|
175
|
-
|
|
200
|
+
config = ClientConfig(
|
|
201
|
+
client_name=_CLIENT_NAME,
|
|
202
|
+
project=self.CDF_PROJECT,
|
|
203
|
+
credentials=self.get_credentials(),
|
|
204
|
+
base_url=self.cdf_url,
|
|
205
|
+
max_workers=self.CDF_MAX_WORKERS,
|
|
206
|
+
timeout=self.CDF_TIMEOUT,
|
|
176
207
|
)
|
|
208
|
+
return CogniteClient(config)
|
|
177
209
|
|
|
178
210
|
def create_env_file(self) -> str:
|
|
179
211
|
lines: list[str] = []
|
|
@@ -194,11 +226,11 @@ class _EnvironmentVariables:
|
|
|
194
226
|
return "\n".join(lines)
|
|
195
227
|
|
|
196
228
|
|
|
197
|
-
def _from_dotenv(evn_file: Path) ->
|
|
229
|
+
def _from_dotenv(evn_file: Path) -> EnvironmentVariables:
|
|
198
230
|
if not evn_file.exists():
|
|
199
231
|
raise FileNotFoundError(f"{evn_file} does not exist.")
|
|
200
232
|
content = evn_file.read_text()
|
|
201
|
-
valid_variables = {f.name for f in fields(
|
|
233
|
+
valid_variables = {f.name for f in fields(EnvironmentVariables)}
|
|
202
234
|
variables: dict[str, str] = {}
|
|
203
235
|
for line in content.splitlines():
|
|
204
236
|
if line.startswith("#") or "=" not in line:
|
|
@@ -206,15 +238,15 @@ def _from_dotenv(evn_file: Path) -> _EnvironmentVariables:
|
|
|
206
238
|
key, value = line.split("=", 1)
|
|
207
239
|
if key in valid_variables:
|
|
208
240
|
variables[key] = value
|
|
209
|
-
return
|
|
241
|
+
return EnvironmentVariables(**variables) # type: ignore[arg-type]
|
|
210
242
|
|
|
211
243
|
|
|
212
|
-
def _prompt_user() ->
|
|
244
|
+
def _prompt_user() -> EnvironmentVariables:
|
|
213
245
|
local_import("rich", "jupyter")
|
|
214
246
|
from rich.prompt import Prompt
|
|
215
247
|
|
|
216
248
|
try:
|
|
217
|
-
variables =
|
|
249
|
+
variables = EnvironmentVariables.create_from_environ()
|
|
218
250
|
continue_ = Prompt.ask(
|
|
219
251
|
f"Use environment variables for CDF Cluster '{variables.CDF_CLUSTER}' "
|
|
220
252
|
f"and Project '{variables.CDF_PROJECT}'? [y/n]",
|
|
@@ -263,25 +295,12 @@ def _prompt_user() -> _EnvironmentVariables:
|
|
|
263
295
|
return variables
|
|
264
296
|
|
|
265
297
|
|
|
266
|
-
def _prompt_cluster_and_project() ->
|
|
298
|
+
def _prompt_cluster_and_project() -> EnvironmentVariables:
|
|
267
299
|
from rich.prompt import Prompt
|
|
268
300
|
|
|
269
301
|
cluster = Prompt.ask("Enter CDF Cluster (example 'greenfield', 'bluefield', 'westeurope-1)")
|
|
270
302
|
project = Prompt.ask("Enter CDF Project")
|
|
271
|
-
return
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def _is_notebook() -> bool:
|
|
275
|
-
try:
|
|
276
|
-
shell = get_ipython().__class__.__name__ # type: ignore[name-defined]
|
|
277
|
-
if shell == "ZMQInteractiveShell":
|
|
278
|
-
return True # Jupyter notebook or qtconsole
|
|
279
|
-
elif shell == "TerminalInteractiveShell":
|
|
280
|
-
return False # Terminal running IPython
|
|
281
|
-
else:
|
|
282
|
-
return False # Other type (?)
|
|
283
|
-
except NameError:
|
|
284
|
-
return False # Probably standard Python interpreter
|
|
303
|
+
return EnvironmentVariables(cluster, project)
|
|
285
304
|
|
|
286
305
|
|
|
287
306
|
def _repo_root() -> Path | None:
|
cognite/neat/utils/auxiliary.py
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
import hashlib
|
|
1
2
|
import importlib
|
|
2
3
|
import inspect
|
|
3
|
-
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import Callable, Iterable
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from functools import wraps
|
|
4
9
|
from types import ModuleType
|
|
5
10
|
|
|
11
|
+
from cognite.client.exceptions import CogniteDuplicatedError, CogniteReadTimeout
|
|
12
|
+
from pydantic_core import ErrorDetails
|
|
13
|
+
|
|
6
14
|
from cognite.neat.exceptions import NeatImportError
|
|
7
15
|
|
|
8
16
|
|
|
@@ -33,3 +41,135 @@ def class_html_doc(cls: type, include_factory_methods: bool = True) -> str:
|
|
|
33
41
|
f'<ul style="list-style-type:circle;">{factory_methods_str}</ul>'
|
|
34
42
|
)
|
|
35
43
|
return f"<h3>{cls.__name__}</h3><p>{docstring}</p>"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def retry_decorator(max_retries=2, retry_delay=3, component_name=""):
|
|
47
|
+
def decorator(func):
|
|
48
|
+
@wraps(func)
|
|
49
|
+
def wrapper(*args, **kwargs):
|
|
50
|
+
previous_exception = None
|
|
51
|
+
for attempt in range(max_retries + 1):
|
|
52
|
+
try:
|
|
53
|
+
logging.debug(f"Attempt {attempt + 1} of {max_retries + 1} for {component_name}")
|
|
54
|
+
return func(*args, **kwargs)
|
|
55
|
+
except CogniteReadTimeout as e:
|
|
56
|
+
previous_exception = e
|
|
57
|
+
if attempt < max_retries:
|
|
58
|
+
logging.error(
|
|
59
|
+
f"""CogniteReadTimeout retry attempt {attempt + 1} failed for {component_name} .
|
|
60
|
+
Retrying in {retry_delay} second(s). Error:"""
|
|
61
|
+
)
|
|
62
|
+
logging.error(e)
|
|
63
|
+
time.sleep(retry_delay)
|
|
64
|
+
else:
|
|
65
|
+
raise e
|
|
66
|
+
except CogniteDuplicatedError as e:
|
|
67
|
+
if isinstance(previous_exception, CogniteReadTimeout):
|
|
68
|
+
# if previous exception was CogniteReadTimeout,
|
|
69
|
+
# we can't be sure if the items were created or not
|
|
70
|
+
if len(e.successful) == 0 and len(e.failed) == 0 and len(e.duplicated) >= 0:
|
|
71
|
+
logging.warning(
|
|
72
|
+
f"Duplicate error for {component_name} . All items already exist in CDF. "
|
|
73
|
+
"Suppressing error."
|
|
74
|
+
)
|
|
75
|
+
return
|
|
76
|
+
else:
|
|
77
|
+
# can happend because of eventual consistency. Retry with delay to allow for CDF to catch up
|
|
78
|
+
if attempt < max_retries:
|
|
79
|
+
logging.error(
|
|
80
|
+
f"""CogniteDuplicatedError retry attempt {attempt + 1} failed for {component_name} .
|
|
81
|
+
Retrying in {retry_delay} second(s). Error:"""
|
|
82
|
+
)
|
|
83
|
+
logging.error(e)
|
|
84
|
+
# incerasing delay to allow for CDF to catch up
|
|
85
|
+
time.sleep(retry_delay)
|
|
86
|
+
else:
|
|
87
|
+
raise e
|
|
88
|
+
else:
|
|
89
|
+
# no point in retrying duplicate error if previous exception was not a timeout
|
|
90
|
+
raise e
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
previous_exception = e
|
|
94
|
+
if attempt < max_retries:
|
|
95
|
+
logging.error(
|
|
96
|
+
f"Retry attempt {attempt + 1} failed for {component_name}. "
|
|
97
|
+
f"Retrying in {retry_delay} second(s)."
|
|
98
|
+
)
|
|
99
|
+
logging.error(e)
|
|
100
|
+
time.sleep(retry_delay)
|
|
101
|
+
else:
|
|
102
|
+
raise e
|
|
103
|
+
|
|
104
|
+
return wrapper
|
|
105
|
+
|
|
106
|
+
return decorator
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def create_sha256_hash(string: str) -> str:
|
|
110
|
+
# Create a SHA-256 hash object
|
|
111
|
+
sha256_hash = hashlib.sha256()
|
|
112
|
+
|
|
113
|
+
# Convert the string to bytes and update the hash object
|
|
114
|
+
sha256_hash.update(string.encode("utf-8"))
|
|
115
|
+
|
|
116
|
+
# Get the hexadecimal representation of the hash
|
|
117
|
+
hash_value = sha256_hash.hexdigest()
|
|
118
|
+
|
|
119
|
+
return hash_value
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# Will likely be removed with legacy code
|
|
123
|
+
def generate_exception_report(exceptions: list[dict] | list[ErrorDetails] | None, category: str = "") -> str:
|
|
124
|
+
exceptions_as_dict = _order_expectations_by_type(exceptions) if exceptions else {}
|
|
125
|
+
report = ""
|
|
126
|
+
|
|
127
|
+
for exception_type in exceptions_as_dict.keys():
|
|
128
|
+
title = f"# {category}: {exception_type}" if category else ""
|
|
129
|
+
warnings = "\n- " + "\n- ".join(exceptions_as_dict[exception_type])
|
|
130
|
+
report += title + warnings + "\n\n"
|
|
131
|
+
|
|
132
|
+
return report
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _order_expectations_by_type(
|
|
136
|
+
exceptions: list[dict] | list[ErrorDetails],
|
|
137
|
+
) -> dict[str, list[str]]:
|
|
138
|
+
exception_dict: dict[str, list[str]] = {}
|
|
139
|
+
for exception in exceptions:
|
|
140
|
+
if not isinstance(exception["loc"], str) and isinstance(exception["loc"], Iterable):
|
|
141
|
+
location = f"[{'/'.join(str(e) for e in exception['loc'])}]"
|
|
142
|
+
else:
|
|
143
|
+
location = ""
|
|
144
|
+
|
|
145
|
+
issue = f"{exception['msg']} {location}"
|
|
146
|
+
|
|
147
|
+
if exception_dict.get(exception["type"]) is None:
|
|
148
|
+
exception_dict[exception["type"]] = [issue]
|
|
149
|
+
else:
|
|
150
|
+
exception_dict[exception["type"]].append(issue)
|
|
151
|
+
return exception_dict
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def string_to_ideal_type(input_string: str) -> int | bool | float | datetime | str:
|
|
155
|
+
try:
|
|
156
|
+
# Try converting to int
|
|
157
|
+
return int(input_string)
|
|
158
|
+
except ValueError:
|
|
159
|
+
try:
|
|
160
|
+
# Try converting to float
|
|
161
|
+
return float(input_string) # type: ignore
|
|
162
|
+
except ValueError:
|
|
163
|
+
if input_string.lower() == "true":
|
|
164
|
+
# Return True if input is 'true'
|
|
165
|
+
return True
|
|
166
|
+
elif input_string.lower() == "false":
|
|
167
|
+
# Return False if input is 'false'
|
|
168
|
+
return False
|
|
169
|
+
else:
|
|
170
|
+
try:
|
|
171
|
+
# Try converting to datetime
|
|
172
|
+
return datetime.fromisoformat(input_string) # type: ignore
|
|
173
|
+
except ValueError:
|
|
174
|
+
# Return the input string if no conversion is possible
|
|
175
|
+
return input_string
|
|
File without changes
|