industrial-model 0.1.9__py3-none-any.whl → 0.1.11__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.
- industrial_model/__init__.py +8 -1
- industrial_model/cognite_adapters/__init__.py +67 -9
- industrial_model/cognite_adapters/aggregation_mapper.py +64 -0
- industrial_model/cognite_adapters/models.py +35 -0
- industrial_model/cognite_adapters/optimizer.py +17 -8
- industrial_model/cognite_adapters/query_result_mapper.py +54 -24
- industrial_model/cognite_adapters/upsert_mapper.py +146 -0
- industrial_model/cognite_adapters/view_mapper.py +38 -4
- industrial_model/config.py +11 -2
- industrial_model/engines/async_engine.py +13 -1
- industrial_model/engines/engine.py +34 -2
- industrial_model/models/__init__.py +8 -0
- industrial_model/models/entities.py +44 -0
- industrial_model/statements/__init__.py +59 -9
- industrial_model/utils.py +1 -12
- {industrial_model-0.1.9.dist-info → industrial_model-0.1.11.dist-info}/METADATA +35 -1
- industrial_model-0.1.11.dist-info/RECORD +31 -0
- industrial_model-0.1.9.dist-info/RECORD +0 -28
- {industrial_model-0.1.9.dist-info → industrial_model-0.1.11.dist-info}/WHEEL +0 -0
industrial_model/__init__.py
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
from .config import DataModelId
|
|
2
2
|
from .engines import AsyncEngine, Engine
|
|
3
3
|
from .models import (
|
|
4
|
+
AggregationResult,
|
|
4
5
|
InstanceId,
|
|
5
6
|
PaginatedResult,
|
|
6
7
|
TViewInstance,
|
|
8
|
+
TWritableViewInstance,
|
|
7
9
|
ValidationMode,
|
|
8
10
|
ViewInstance,
|
|
9
11
|
ViewInstanceConfig,
|
|
12
|
+
WritableViewInstance,
|
|
10
13
|
)
|
|
11
|
-
from .statements import and_, col, not_, or_, select
|
|
14
|
+
from .statements import aggregate, and_, col, not_, or_, select
|
|
12
15
|
|
|
13
16
|
__all__ = [
|
|
17
|
+
"aggregate",
|
|
18
|
+
"AggregationResult",
|
|
14
19
|
"and_",
|
|
15
20
|
"or_",
|
|
16
21
|
"col",
|
|
@@ -20,9 +25,11 @@ __all__ = [
|
|
|
20
25
|
"InstanceId",
|
|
21
26
|
"TViewInstance",
|
|
22
27
|
"DataModelId",
|
|
28
|
+
"TWritableViewInstance",
|
|
23
29
|
"ValidationMode",
|
|
24
30
|
"Engine",
|
|
25
31
|
"AsyncEngine",
|
|
26
32
|
"PaginatedResult",
|
|
27
33
|
"ViewInstanceConfig",
|
|
34
|
+
"WritableViewInstance",
|
|
28
35
|
]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from typing import Any
|
|
2
3
|
|
|
3
4
|
from cognite.client import CogniteClient
|
|
@@ -9,15 +10,21 @@ from cognite.client.data_classes.data_modeling.query import (
|
|
|
9
10
|
QueryResult as CogniteQueryResult,
|
|
10
11
|
)
|
|
11
12
|
|
|
12
|
-
from industrial_model.cognite_adapters.optimizer import QueryOptimizer
|
|
13
13
|
from industrial_model.config import DataModelId
|
|
14
|
-
from industrial_model.models import
|
|
15
|
-
|
|
14
|
+
from industrial_model.models import (
|
|
15
|
+
AggregationResult,
|
|
16
|
+
TViewInstance,
|
|
17
|
+
TWritableViewInstance,
|
|
18
|
+
)
|
|
19
|
+
from industrial_model.statements import AggregationStatement, Statement
|
|
16
20
|
|
|
21
|
+
from .aggregation_mapper import AggregationMapper
|
|
22
|
+
from .optimizer import QueryOptimizer
|
|
17
23
|
from .query_mapper import QueryMapper
|
|
18
24
|
from .query_result_mapper import (
|
|
19
25
|
QueryResultMapper,
|
|
20
26
|
)
|
|
27
|
+
from .upsert_mapper import UpsertMapper
|
|
21
28
|
from .utils import (
|
|
22
29
|
append_nodes_and_edges,
|
|
23
30
|
get_query_for_dependencies_pagination,
|
|
@@ -32,14 +39,12 @@ class CogniteAdapter:
|
|
|
32
39
|
):
|
|
33
40
|
self._cognite_client = cognite_client
|
|
34
41
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
inline_views=True,
|
|
38
|
-
).latest_version()
|
|
39
|
-
view_mapper = ViewMapper(dm.views)
|
|
42
|
+
view_mapper = ViewMapper(cognite_client, data_model_id)
|
|
43
|
+
self._optmizer = QueryOptimizer(cognite_client, data_model_id)
|
|
40
44
|
self._query_mapper = QueryMapper(view_mapper)
|
|
41
45
|
self._result_mapper = QueryResultMapper(view_mapper)
|
|
42
|
-
self.
|
|
46
|
+
self._upsert_mapper = UpsertMapper(view_mapper)
|
|
47
|
+
self._aggregation_mapper = AggregationMapper(view_mapper)
|
|
43
48
|
|
|
44
49
|
def query(
|
|
45
50
|
self, statement: Statement[TViewInstance], all_pages: bool
|
|
@@ -77,6 +82,59 @@ class CogniteAdapter:
|
|
|
77
82
|
if not all_pages or last_page:
|
|
78
83
|
return data, next_cursor_
|
|
79
84
|
|
|
85
|
+
def aggregate(
|
|
86
|
+
self, statement: AggregationStatement[TViewInstance]
|
|
87
|
+
) -> list[AggregationResult]:
|
|
88
|
+
query = self._aggregation_mapper.map(statement)
|
|
89
|
+
|
|
90
|
+
result = self._cognite_client.data_modeling.instances.aggregate(
|
|
91
|
+
view=query.view.as_id(),
|
|
92
|
+
aggregates=query.metric_aggregation,
|
|
93
|
+
filter=query.filters,
|
|
94
|
+
group_by=query.group_by_columns,
|
|
95
|
+
limit=query.limit,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return [
|
|
99
|
+
AggregationResult(
|
|
100
|
+
group=item.group,
|
|
101
|
+
value=item.aggregates[0].value,
|
|
102
|
+
aggregate=statement.aggregate_,
|
|
103
|
+
)
|
|
104
|
+
for item in result
|
|
105
|
+
if item.aggregates and item.aggregates[0].value is not None
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
def upsert(
|
|
109
|
+
self, entries: list[TWritableViewInstance], replace: bool = False
|
|
110
|
+
) -> None:
|
|
111
|
+
logger = logging.getLogger(__name__)
|
|
112
|
+
operation = self._upsert_mapper.map(entries)
|
|
113
|
+
|
|
114
|
+
for node_chunk in operation.chunk_nodes():
|
|
115
|
+
logger.info(
|
|
116
|
+
f"Upserting {len(node_chunk)} nodes (replace={replace})"
|
|
117
|
+
)
|
|
118
|
+
self._cognite_client.data_modeling.instances.apply(
|
|
119
|
+
nodes=node_chunk,
|
|
120
|
+
replace=replace,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
for edge_chunk in operation.chunk_edges():
|
|
124
|
+
logger.info(
|
|
125
|
+
f"Upserting {len(edge_chunk)} edges (replace={replace})"
|
|
126
|
+
)
|
|
127
|
+
self._cognite_client.data_modeling.instances.apply(
|
|
128
|
+
edges=edge_chunk,
|
|
129
|
+
replace=replace,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
for edges_to_remove_chunk in operation.chunk_edges_to_delete():
|
|
133
|
+
logger.info(f"Deleting {len(edges_to_remove_chunk)} edges")
|
|
134
|
+
self._cognite_client.data_modeling.instances.delete(
|
|
135
|
+
edges=[item.as_tuple() for item in edges_to_remove_chunk],
|
|
136
|
+
)
|
|
137
|
+
|
|
80
138
|
def _query_dependencies_pages(
|
|
81
139
|
self,
|
|
82
140
|
cognite_query: CogniteQuery,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
import cognite.client.data_classes.filters as filters
|
|
4
|
+
from cognite.client.data_classes.aggregations import Count, MetricAggregation
|
|
5
|
+
from cognite.client.data_classes.data_modeling import (
|
|
6
|
+
View,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from industrial_model.models import TViewInstance
|
|
10
|
+
from industrial_model.statements import AggregationStatement
|
|
11
|
+
|
|
12
|
+
from .filter_mapper import (
|
|
13
|
+
FilterMapper,
|
|
14
|
+
)
|
|
15
|
+
from .view_mapper import ViewMapper
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class AggregationQuery:
|
|
20
|
+
view: View
|
|
21
|
+
metric_aggregation: MetricAggregation
|
|
22
|
+
filters: filters.Filter | None
|
|
23
|
+
group_by_columns: list[str]
|
|
24
|
+
limit: int
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AggregationMapper:
|
|
28
|
+
def __init__(self, view_mapper: ViewMapper):
|
|
29
|
+
self._view_mapper = view_mapper
|
|
30
|
+
self._filter_mapper = FilterMapper(view_mapper)
|
|
31
|
+
|
|
32
|
+
def map(
|
|
33
|
+
self, statement: AggregationStatement[TViewInstance]
|
|
34
|
+
) -> AggregationQuery:
|
|
35
|
+
root_node = statement.entity.get_view_external_id()
|
|
36
|
+
|
|
37
|
+
root_view = self._view_mapper.get_view(root_node)
|
|
38
|
+
|
|
39
|
+
filters_ = (
|
|
40
|
+
filters.And(
|
|
41
|
+
*self._filter_mapper.map(statement.where_clauses, root_view)
|
|
42
|
+
)
|
|
43
|
+
if statement.where_clauses
|
|
44
|
+
else None
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
metric_aggregation = (
|
|
48
|
+
Count(statement.aggregation_property.property)
|
|
49
|
+
if statement.aggregate_ == "count"
|
|
50
|
+
else None
|
|
51
|
+
)
|
|
52
|
+
if metric_aggregation is None:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"Unsupported aggregate function: {statement.aggregate_}"
|
|
55
|
+
)
|
|
56
|
+
return AggregationQuery(
|
|
57
|
+
view=root_view,
|
|
58
|
+
metric_aggregation=metric_aggregation,
|
|
59
|
+
filters=filters_,
|
|
60
|
+
group_by_columns=[
|
|
61
|
+
column.property for column in statement.group_by_columns
|
|
62
|
+
],
|
|
63
|
+
limit=statement.limit_,
|
|
64
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from cognite.client.data_classes.data_modeling import (
|
|
5
|
+
EdgeApply,
|
|
6
|
+
NodeApply,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from industrial_model.models.entities import EdgeContainer
|
|
10
|
+
|
|
11
|
+
_PAGE_SIZE = 1000
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class UpsertOperation:
|
|
16
|
+
nodes: list[NodeApply]
|
|
17
|
+
edges: list[EdgeApply]
|
|
18
|
+
edges_to_delete: list[EdgeContainer]
|
|
19
|
+
|
|
20
|
+
def chunk_nodes(self) -> list[list[NodeApply]]:
|
|
21
|
+
return self._chunk_list(self.nodes)
|
|
22
|
+
|
|
23
|
+
def chunk_edges(self) -> list[list[EdgeApply]]:
|
|
24
|
+
return self._chunk_list(self.edges)
|
|
25
|
+
|
|
26
|
+
def chunk_edges_to_delete(self) -> list[list[EdgeContainer]]:
|
|
27
|
+
return self._chunk_list(self.edges_to_delete)
|
|
28
|
+
|
|
29
|
+
def _chunk_list(self, entries: list[Any]) -> list[list[Any]]:
|
|
30
|
+
data: list[list[Any]] = []
|
|
31
|
+
for i in range(0, len(entries), _PAGE_SIZE):
|
|
32
|
+
start = i
|
|
33
|
+
end = i + _PAGE_SIZE
|
|
34
|
+
data.append(entries[start:end])
|
|
35
|
+
return data
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
from threading import Lock
|
|
2
|
+
|
|
1
3
|
from cognite.client import CogniteClient
|
|
2
4
|
|
|
5
|
+
from industrial_model.config import DataModelId
|
|
3
6
|
from industrial_model.models import TViewInstance
|
|
4
7
|
from industrial_model.statements import (
|
|
5
8
|
BoolExpression,
|
|
@@ -14,11 +17,11 @@ SPACE_PROPERTY = "space"
|
|
|
14
17
|
|
|
15
18
|
class QueryOptimizer:
|
|
16
19
|
def __init__(
|
|
17
|
-
self,
|
|
18
|
-
cognite_client: CogniteClient,
|
|
20
|
+
self, cognite_client: CogniteClient, data_model_id: DataModelId
|
|
19
21
|
):
|
|
20
|
-
self._all_spaces
|
|
22
|
+
self._all_spaces = data_model_id.instance_spaces
|
|
21
23
|
self._cognite_client = cognite_client
|
|
24
|
+
self._lock = Lock()
|
|
22
25
|
|
|
23
26
|
def optimize(self, statement: Statement[TViewInstance]) -> None:
|
|
24
27
|
instance_spaces = statement.entity.view_config.get("instance_spaces")
|
|
@@ -58,7 +61,7 @@ class QueryOptimizer:
|
|
|
58
61
|
return False
|
|
59
62
|
|
|
60
63
|
def _find_spaces(self, instance_spaces_prefix: str) -> list[str]:
|
|
61
|
-
all_spaces = self.
|
|
64
|
+
all_spaces = self._load_spaces()
|
|
62
65
|
|
|
63
66
|
return [
|
|
64
67
|
space
|
|
@@ -66,12 +69,18 @@ class QueryOptimizer:
|
|
|
66
69
|
if space.startswith(instance_spaces_prefix)
|
|
67
70
|
]
|
|
68
71
|
|
|
69
|
-
def
|
|
72
|
+
def _load_spaces(self) -> list[str]:
|
|
70
73
|
all_spaces = self._all_spaces
|
|
71
|
-
if all_spaces
|
|
74
|
+
if all_spaces:
|
|
75
|
+
return all_spaces
|
|
76
|
+
|
|
77
|
+
with self._lock:
|
|
78
|
+
if self._all_spaces:
|
|
79
|
+
return self._all_spaces
|
|
80
|
+
|
|
72
81
|
all_spaces = self._cognite_client.data_modeling.spaces.list(
|
|
73
82
|
limit=-1
|
|
74
83
|
).as_ids()
|
|
75
84
|
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
self._all_spaces = all_spaces
|
|
86
|
+
return all_spaces
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from collections import defaultdict
|
|
2
|
-
from typing import Any
|
|
2
|
+
from typing import Any, TypedDict
|
|
3
3
|
|
|
4
4
|
from cognite.client.data_classes.data_modeling import (
|
|
5
5
|
Edge,
|
|
@@ -14,10 +14,17 @@ from cognite.client.data_classes.data_modeling.views import (
|
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
from industrial_model.constants import EDGE_DIRECTION, EDGE_MARKER, NESTED_SEP
|
|
17
|
+
from industrial_model.models import EdgeContainer
|
|
17
18
|
|
|
18
19
|
from .view_mapper import ViewMapper
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
class _PropertyMapping(TypedDict):
|
|
23
|
+
is_list: bool
|
|
24
|
+
nodes: dict[tuple[str, str], list[Node]]
|
|
25
|
+
edges: dict[tuple[str, str], list[Edge]]
|
|
26
|
+
|
|
27
|
+
|
|
21
28
|
class QueryResultMapper:
|
|
22
29
|
def __init__(self, view_mapper: ViewMapper):
|
|
23
30
|
self._view_mapper = view_mapper
|
|
@@ -79,8 +86,10 @@ class QueryResultMapper:
|
|
|
79
86
|
|
|
80
87
|
visited.add(identify)
|
|
81
88
|
properties = node.properties.get(view_id, {})
|
|
89
|
+
|
|
90
|
+
edges_mapping: dict[str, list[EdgeContainer]] = {}
|
|
82
91
|
node_id = get_node_id(node)
|
|
83
|
-
for mapping_key,
|
|
92
|
+
for mapping_key, mapping_value in mappings.items():
|
|
84
93
|
element = properties.get(mapping_key)
|
|
85
94
|
|
|
86
95
|
element_key: tuple[str, str] = (
|
|
@@ -89,15 +98,24 @@ class QueryResultMapper:
|
|
|
89
98
|
else (node.space, node.external_id)
|
|
90
99
|
)
|
|
91
100
|
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
mapping_nodes = mapping_value.get("nodes", {})
|
|
102
|
+
mapping_edges = mapping_value.get("edges", {})
|
|
103
|
+
is_list = mapping_value.get("is_list", False)
|
|
104
|
+
|
|
105
|
+
node_entries = mapping_nodes.get(element_key)
|
|
106
|
+
if not node_entries:
|
|
94
107
|
continue
|
|
95
108
|
|
|
96
|
-
entry_data = self._nodes_to_dict(
|
|
109
|
+
entry_data = self._nodes_to_dict(node_entries)
|
|
97
110
|
properties[mapping_key] = (
|
|
98
111
|
entry_data if is_list else entry_data[0]
|
|
99
112
|
)
|
|
100
|
-
|
|
113
|
+
edge_entries = mapping_edges.get(element_key)
|
|
114
|
+
if edge_entries:
|
|
115
|
+
edges_mapping[mapping_key] = self._edges_to_model(
|
|
116
|
+
edge_entries
|
|
117
|
+
)
|
|
118
|
+
properties["_edges"] = edges_mapping
|
|
101
119
|
node.properties[view_id] = properties
|
|
102
120
|
|
|
103
121
|
result[node_id].append(node)
|
|
@@ -109,19 +127,18 @@ class QueryResultMapper:
|
|
|
109
127
|
key: str,
|
|
110
128
|
view: View,
|
|
111
129
|
query_result: dict[str, list[Node | Edge]],
|
|
112
|
-
) -> dict[str,
|
|
113
|
-
mappings: dict[
|
|
114
|
-
str, tuple[dict[tuple[str, str], list[Node]], bool]
|
|
115
|
-
] = {}
|
|
130
|
+
) -> dict[str, _PropertyMapping]:
|
|
131
|
+
mappings: dict[str, _PropertyMapping] = {}
|
|
116
132
|
|
|
117
133
|
for property_name, property in view.properties.items():
|
|
118
134
|
property_key = f"{key}{NESTED_SEP}{property_name}"
|
|
119
135
|
|
|
120
|
-
|
|
136
|
+
nodes: dict[tuple[str, str], list[Node]] | None = None
|
|
137
|
+
edges: dict[tuple[str, str], list[Edge]] | None = None
|
|
121
138
|
is_list = False
|
|
122
139
|
|
|
123
140
|
if isinstance(property, MappedProperty) and property.source:
|
|
124
|
-
|
|
141
|
+
nodes = self._map_node_property(
|
|
125
142
|
property_key,
|
|
126
143
|
self._view_mapper.get_view(property.source.external_id),
|
|
127
144
|
query_result,
|
|
@@ -131,7 +148,7 @@ class QueryResultMapper:
|
|
|
131
148
|
isinstance(property, SingleReverseDirectRelation)
|
|
132
149
|
and property.source
|
|
133
150
|
):
|
|
134
|
-
|
|
151
|
+
nodes = self._map_node_property(
|
|
135
152
|
property_key,
|
|
136
153
|
self._view_mapper.get_view(property.source.external_id),
|
|
137
154
|
query_result,
|
|
@@ -142,7 +159,7 @@ class QueryResultMapper:
|
|
|
142
159
|
isinstance(property, MultiReverseDirectRelation)
|
|
143
160
|
and property.source
|
|
144
161
|
):
|
|
145
|
-
|
|
162
|
+
nodes = self._map_node_property(
|
|
146
163
|
property_key,
|
|
147
164
|
self._view_mapper.get_view(property.source.external_id),
|
|
148
165
|
query_result,
|
|
@@ -151,7 +168,7 @@ class QueryResultMapper:
|
|
|
151
168
|
is_list = True
|
|
152
169
|
|
|
153
170
|
elif isinstance(property, EdgeConnection) and property.source:
|
|
154
|
-
|
|
171
|
+
nodes, edges = self._map_edge_property(
|
|
155
172
|
property_key,
|
|
156
173
|
self._view_mapper.get_view(property.source.external_id),
|
|
157
174
|
query_result,
|
|
@@ -159,8 +176,10 @@ class QueryResultMapper:
|
|
|
159
176
|
)
|
|
160
177
|
is_list = True
|
|
161
178
|
|
|
162
|
-
if
|
|
163
|
-
mappings[property_name] =
|
|
179
|
+
if nodes:
|
|
180
|
+
mappings[property_name] = _PropertyMapping(
|
|
181
|
+
is_list=is_list, nodes=nodes, edges=edges or {}
|
|
182
|
+
)
|
|
164
183
|
|
|
165
184
|
return mappings
|
|
166
185
|
|
|
@@ -170,17 +189,25 @@ class QueryResultMapper:
|
|
|
170
189
|
view: View,
|
|
171
190
|
query_result: dict[str, list[Node | Edge]],
|
|
172
191
|
edge_direction: EDGE_DIRECTION,
|
|
173
|
-
) ->
|
|
192
|
+
) -> tuple[
|
|
193
|
+
dict[tuple[str, str], list[Node]] | None,
|
|
194
|
+
dict[tuple[str, str], list[Edge]] | None,
|
|
195
|
+
]:
|
|
174
196
|
edge_key = f"{key}{NESTED_SEP}{EDGE_MARKER}"
|
|
175
197
|
if key not in query_result or edge_key not in query_result:
|
|
176
|
-
return None
|
|
198
|
+
return None, None
|
|
177
199
|
|
|
178
200
|
nodes = self._map_node_property(key, view, query_result)
|
|
179
201
|
if not nodes:
|
|
180
|
-
return None
|
|
202
|
+
return None, None
|
|
181
203
|
|
|
182
204
|
visited: set[tuple[str, str]] = set()
|
|
183
|
-
|
|
205
|
+
nodes_result: defaultdict[tuple[str, str], list[Node]] = defaultdict(
|
|
206
|
+
list
|
|
207
|
+
)
|
|
208
|
+
edges_result: defaultdict[tuple[str, str], list[Edge]] = defaultdict(
|
|
209
|
+
list
|
|
210
|
+
)
|
|
184
211
|
for edge in query_result[edge_key]:
|
|
185
212
|
identify = (edge.space, edge.external_id)
|
|
186
213
|
if not isinstance(edge, Edge) or identify in visited:
|
|
@@ -195,15 +222,18 @@ class QueryResultMapper:
|
|
|
195
222
|
if edge_direction == "inwards"
|
|
196
223
|
else (edge.start_node.as_tuple(), edge.end_node.as_tuple())
|
|
197
224
|
)
|
|
198
|
-
|
|
225
|
+
edges_result[entry_key].append(edge)
|
|
199
226
|
if node_item := nodes.get(node_key):
|
|
200
|
-
|
|
227
|
+
nodes_result[entry_key].extend(node_item)
|
|
201
228
|
|
|
202
|
-
return dict(
|
|
229
|
+
return dict(nodes_result), dict(edges_result)
|
|
203
230
|
|
|
204
231
|
def _nodes_to_dict(self, nodes: list[Node]) -> list[dict[str, Any]]:
|
|
205
232
|
return [self._node_to_dict(node) for node in nodes]
|
|
206
233
|
|
|
234
|
+
def _edges_to_model(self, edges: list[Edge]) -> list[EdgeContainer]:
|
|
235
|
+
return [EdgeContainer.model_validate(edge) for edge in edges]
|
|
236
|
+
|
|
207
237
|
def _node_to_dict(self, node: Node) -> dict[str, Any]:
|
|
208
238
|
entry = node.dump()
|
|
209
239
|
properties: dict[str, dict[str, dict[str, Any]]] = (
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from cognite.client.data_classes.data_modeling import (
|
|
5
|
+
DirectRelationReference,
|
|
6
|
+
EdgeApply,
|
|
7
|
+
EdgeConnection,
|
|
8
|
+
MappedProperty,
|
|
9
|
+
NodeApply,
|
|
10
|
+
NodeOrEdgeData,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from industrial_model.cognite_adapters.models import UpsertOperation
|
|
14
|
+
from industrial_model.models import (
|
|
15
|
+
EdgeContainer,
|
|
16
|
+
InstanceId,
|
|
17
|
+
TWritableViewInstance,
|
|
18
|
+
)
|
|
19
|
+
from industrial_model.utils import datetime_to_ms_iso_timestamp
|
|
20
|
+
|
|
21
|
+
from .view_mapper import ViewMapper
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UpsertMapper:
|
|
25
|
+
def __init__(self, view_mapper: ViewMapper):
|
|
26
|
+
self._view_mapper = view_mapper
|
|
27
|
+
|
|
28
|
+
def map(self, instances: list[TWritableViewInstance]) -> UpsertOperation:
|
|
29
|
+
nodes: dict[tuple[str, str], NodeApply] = {}
|
|
30
|
+
edges: dict[tuple[str, str], EdgeApply] = {}
|
|
31
|
+
edges_to_delete: dict[tuple[str, str], EdgeContainer] = {}
|
|
32
|
+
|
|
33
|
+
for instance in instances:
|
|
34
|
+
entry_nodes, entry_edges, entry_edges_to_delete = (
|
|
35
|
+
self._map_instance(instance)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
nodes[instance.as_tuple()] = entry_nodes
|
|
39
|
+
edges.update(
|
|
40
|
+
{(item.space, item.external_id): item for item in entry_edges}
|
|
41
|
+
)
|
|
42
|
+
edges_to_delete.update(
|
|
43
|
+
{
|
|
44
|
+
(item.space, item.external_id): item
|
|
45
|
+
for item in entry_edges_to_delete
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return UpsertOperation(
|
|
50
|
+
nodes=list(nodes.values()),
|
|
51
|
+
edges=list(edges.values()),
|
|
52
|
+
edges_to_delete=list(edges_to_delete.values()),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def _map_instance(
|
|
56
|
+
self, instance: TWritableViewInstance
|
|
57
|
+
) -> tuple[NodeApply, list[EdgeApply], list[EdgeContainer]]:
|
|
58
|
+
view = self._view_mapper.get_view(instance.get_view_external_id())
|
|
59
|
+
|
|
60
|
+
edges: list[EdgeApply] = []
|
|
61
|
+
edges_to_delete: list[EdgeContainer] = []
|
|
62
|
+
properties: dict[str, Any] = {}
|
|
63
|
+
for property_name, property in view.properties.items():
|
|
64
|
+
property_key = instance.get_field_name(property_name)
|
|
65
|
+
if not property_key:
|
|
66
|
+
continue
|
|
67
|
+
entry = instance.__getattribute__(property_key)
|
|
68
|
+
|
|
69
|
+
if isinstance(property, MappedProperty):
|
|
70
|
+
properties[property_name] = (
|
|
71
|
+
DirectRelationReference(
|
|
72
|
+
space=entry.space, external_id=entry.external_id
|
|
73
|
+
)
|
|
74
|
+
if isinstance(entry, InstanceId)
|
|
75
|
+
else datetime_to_ms_iso_timestamp(entry)
|
|
76
|
+
if isinstance(entry, datetime.datetime)
|
|
77
|
+
else entry
|
|
78
|
+
)
|
|
79
|
+
elif isinstance(property, EdgeConnection) and isinstance(
|
|
80
|
+
entry, list
|
|
81
|
+
):
|
|
82
|
+
possible_entries = self._map_edges(instance, property, entry)
|
|
83
|
+
|
|
84
|
+
previous_edges = {
|
|
85
|
+
item.as_tuple(): item
|
|
86
|
+
for item in instance._edges.get(property_name, [])
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
new_entries = [
|
|
90
|
+
edge
|
|
91
|
+
for edge_id, edge in possible_entries.items()
|
|
92
|
+
if edge_id not in previous_edges
|
|
93
|
+
]
|
|
94
|
+
edges_to_delete.extend(
|
|
95
|
+
[
|
|
96
|
+
previous_edges[edge_id]
|
|
97
|
+
for edge_id in previous_edges
|
|
98
|
+
if edge_id not in possible_entries
|
|
99
|
+
]
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
edges.extend(new_entries)
|
|
103
|
+
|
|
104
|
+
node = NodeApply(
|
|
105
|
+
external_id=instance.external_id,
|
|
106
|
+
space=instance.space,
|
|
107
|
+
sources=[
|
|
108
|
+
NodeOrEdgeData(source=view.as_id(), properties=properties)
|
|
109
|
+
],
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return node, edges, edges_to_delete
|
|
113
|
+
|
|
114
|
+
def _map_edges(
|
|
115
|
+
self,
|
|
116
|
+
instance: TWritableViewInstance,
|
|
117
|
+
property: EdgeConnection,
|
|
118
|
+
values: list[Any],
|
|
119
|
+
) -> dict[tuple[str, str], EdgeApply]:
|
|
120
|
+
edge_type = InstanceId.model_validate(property.type)
|
|
121
|
+
|
|
122
|
+
result: dict[tuple[str, str], EdgeApply] = {}
|
|
123
|
+
for value in values:
|
|
124
|
+
if not isinstance(value, InstanceId):
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"""Invalid value for edge property {property.name}:
|
|
127
|
+
Received {type(value)} | Expected: InstanceId"""
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
start_node, end_node = (
|
|
131
|
+
(instance, value)
|
|
132
|
+
if property.direction == "outwards"
|
|
133
|
+
else (value, instance)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
edge_id = instance.edge_id_factory(value, edge_type)
|
|
137
|
+
|
|
138
|
+
result[edge_id.as_tuple()] = EdgeApply(
|
|
139
|
+
external_id=edge_id.external_id,
|
|
140
|
+
space=edge_id.space,
|
|
141
|
+
type=property.type,
|
|
142
|
+
start_node=start_node.as_tuple(),
|
|
143
|
+
end_node=end_node.as_tuple(),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return result
|
|
@@ -1,16 +1,50 @@
|
|
|
1
|
+
from threading import Lock
|
|
2
|
+
|
|
3
|
+
from cognite.client import CogniteClient
|
|
1
4
|
from cognite.client.data_classes.data_modeling import (
|
|
2
5
|
View,
|
|
3
6
|
)
|
|
4
7
|
|
|
8
|
+
from industrial_model.config import DataModelId
|
|
9
|
+
|
|
5
10
|
|
|
6
11
|
class ViewMapper:
|
|
7
|
-
def __init__(
|
|
8
|
-
self
|
|
12
|
+
def __init__(
|
|
13
|
+
self, cognite_client: CogniteClient, data_model_id: DataModelId
|
|
14
|
+
):
|
|
15
|
+
self._cognite_client = cognite_client
|
|
16
|
+
self._data_model_id = data_model_id
|
|
17
|
+
self._views_as_dict: dict[str, View] | None = None
|
|
18
|
+
|
|
19
|
+
if data_model_id.views:
|
|
20
|
+
self._views_as_dict = {
|
|
21
|
+
view.external_id: view for view in data_model_id.views
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
self._lock = Lock()
|
|
9
25
|
|
|
10
26
|
def get_view(self, view_external_id: str) -> View:
|
|
11
|
-
|
|
27
|
+
views = self._load_views()
|
|
28
|
+
if view_external_id not in views:
|
|
12
29
|
raise ValueError(
|
|
13
30
|
f"View {view_external_id} is not available in data model"
|
|
14
31
|
)
|
|
15
32
|
|
|
16
|
-
return
|
|
33
|
+
return views[view_external_id]
|
|
34
|
+
|
|
35
|
+
def _load_views(self) -> dict[str, View]:
|
|
36
|
+
if self._views_as_dict:
|
|
37
|
+
return self._views_as_dict
|
|
38
|
+
|
|
39
|
+
with self._lock:
|
|
40
|
+
if self._views_as_dict:
|
|
41
|
+
return self._views_as_dict
|
|
42
|
+
|
|
43
|
+
dm = self._cognite_client.data_modeling.data_models.retrieve(
|
|
44
|
+
ids=self._data_model_id.as_tuple(),
|
|
45
|
+
inline_views=True,
|
|
46
|
+
).latest_version()
|
|
47
|
+
|
|
48
|
+
views = {view.external_id: view for view in dm.views}
|
|
49
|
+
self._views_as_dict = views
|
|
50
|
+
return views
|
industrial_model/config.py
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
from
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
2
|
|
|
3
|
+
from cognite.client.data_classes.data_modeling import View
|
|
4
|
+
from pydantic import BaseModel, ConfigDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class DataModelId(BaseModel):
|
|
9
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
3
10
|
|
|
4
|
-
class DataModelId(RootModel):
|
|
5
11
|
external_id: str
|
|
6
12
|
space: str
|
|
7
13
|
version: str
|
|
8
14
|
|
|
15
|
+
views: list[View] | None = field(default=None)
|
|
16
|
+
instance_spaces: list[str] | None = field(default=None)
|
|
17
|
+
|
|
9
18
|
def as_tuple(self) -> tuple[str, str, str]:
|
|
10
19
|
return self.space, self.external_id, self.version
|
|
@@ -2,11 +2,13 @@ from cognite.client import CogniteClient
|
|
|
2
2
|
|
|
3
3
|
from industrial_model.config import DataModelId
|
|
4
4
|
from industrial_model.models import (
|
|
5
|
+
AggregationResult,
|
|
5
6
|
PaginatedResult,
|
|
6
7
|
TViewInstance,
|
|
8
|
+
TWritableViewInstance,
|
|
7
9
|
ValidationMode,
|
|
8
10
|
)
|
|
9
|
-
from industrial_model.statements import Statement
|
|
11
|
+
from industrial_model.statements import AggregationStatement, Statement
|
|
10
12
|
from industrial_model.utils import run_async
|
|
11
13
|
|
|
12
14
|
from .engine import Engine
|
|
@@ -35,3 +37,13 @@ class AsyncEngine:
|
|
|
35
37
|
return await run_async(
|
|
36
38
|
self._engine.query_all_pages, statement, validation_mode
|
|
37
39
|
)
|
|
40
|
+
|
|
41
|
+
async def aggregate_async(
|
|
42
|
+
self, statement: AggregationStatement[TViewInstance]
|
|
43
|
+
) -> list[AggregationResult]:
|
|
44
|
+
return await run_async(self._engine.aggregate, statement)
|
|
45
|
+
|
|
46
|
+
async def upsert_async(
|
|
47
|
+
self, entries: list[TWritableViewInstance], replace: bool = False
|
|
48
|
+
) -> None:
|
|
49
|
+
return await run_async(self._engine.upsert, entries, replace)
|
|
@@ -5,11 +5,14 @@ from cognite.client import CogniteClient
|
|
|
5
5
|
from industrial_model.cognite_adapters import CogniteAdapter
|
|
6
6
|
from industrial_model.config import DataModelId
|
|
7
7
|
from industrial_model.models import (
|
|
8
|
+
AggregationResult,
|
|
9
|
+
EdgeContainer,
|
|
8
10
|
PaginatedResult,
|
|
9
11
|
TViewInstance,
|
|
12
|
+
TWritableViewInstance,
|
|
10
13
|
ValidationMode,
|
|
11
14
|
)
|
|
12
|
-
from industrial_model.statements import Statement
|
|
15
|
+
from industrial_model.statements import AggregationStatement, Statement
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
class Engine:
|
|
@@ -45,6 +48,19 @@ class Engine:
|
|
|
45
48
|
|
|
46
49
|
return self._validate_data(statement.entity, data, validation_mode)
|
|
47
50
|
|
|
51
|
+
def aggregate(
|
|
52
|
+
self, statement: AggregationStatement[TViewInstance]
|
|
53
|
+
) -> list[AggregationResult]:
|
|
54
|
+
return self._cognite_adapter.aggregate(statement)
|
|
55
|
+
|
|
56
|
+
def upsert(
|
|
57
|
+
self, entries: list[TWritableViewInstance], replace: bool = False
|
|
58
|
+
) -> None:
|
|
59
|
+
if not entries:
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
return self._cognite_adapter.upsert(entries, replace)
|
|
63
|
+
|
|
48
64
|
def _validate_data(
|
|
49
65
|
self,
|
|
50
66
|
entity: type[TViewInstance],
|
|
@@ -54,9 +70,25 @@ class Engine:
|
|
|
54
70
|
result: list[TViewInstance] = []
|
|
55
71
|
for item in data:
|
|
56
72
|
try:
|
|
57
|
-
|
|
73
|
+
validated_item = entity.model_validate(item)
|
|
74
|
+
self._include_edges(item, validated_item)
|
|
75
|
+
result.append(validated_item)
|
|
58
76
|
except Exception:
|
|
59
77
|
if validation_mode == "ignoreOnError":
|
|
60
78
|
continue
|
|
61
79
|
raise
|
|
62
80
|
return result
|
|
81
|
+
|
|
82
|
+
def _include_edges(
|
|
83
|
+
self, item: dict[str, Any], validated_item: TViewInstance
|
|
84
|
+
) -> None:
|
|
85
|
+
if "_edges" not in item or not isinstance(item["_edges"], dict):
|
|
86
|
+
return
|
|
87
|
+
entries: dict[str, list[EdgeContainer]] = {}
|
|
88
|
+
for property_, edges in item["_edges"].items():
|
|
89
|
+
if not edges or not isinstance(edges, list):
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
assert isinstance(edges[0], EdgeContainer)
|
|
93
|
+
entries[property_] = edges
|
|
94
|
+
validated_item._edges = entries
|
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
from .base import RootModel
|
|
2
2
|
from .entities import (
|
|
3
|
+
AggregationResult,
|
|
4
|
+
EdgeContainer,
|
|
3
5
|
InstanceId,
|
|
4
6
|
PaginatedResult,
|
|
5
7
|
TViewInstance,
|
|
8
|
+
TWritableViewInstance,
|
|
6
9
|
ValidationMode,
|
|
7
10
|
ViewInstance,
|
|
8
11
|
ViewInstanceConfig,
|
|
12
|
+
WritableViewInstance,
|
|
9
13
|
)
|
|
10
14
|
from .schemas import get_parent_and_children_nodes, get_schema_properties
|
|
11
15
|
|
|
12
16
|
__all__ = [
|
|
17
|
+
"AggregationResult",
|
|
13
18
|
"RootModel",
|
|
19
|
+
"EdgeContainer",
|
|
14
20
|
"InstanceId",
|
|
15
21
|
"TViewInstance",
|
|
22
|
+
"TWritableViewInstance",
|
|
16
23
|
"ViewInstance",
|
|
17
24
|
"ValidationMode",
|
|
18
25
|
"PaginatedResult",
|
|
19
26
|
"ViewInstanceConfig",
|
|
20
27
|
"get_schema_properties",
|
|
21
28
|
"get_parent_and_children_nodes",
|
|
29
|
+
"WritableViewInstance",
|
|
22
30
|
]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
1
2
|
from typing import (
|
|
2
3
|
Any,
|
|
3
4
|
ClassVar,
|
|
@@ -7,6 +8,8 @@ from typing import (
|
|
|
7
8
|
TypeVar,
|
|
8
9
|
)
|
|
9
10
|
|
|
11
|
+
from pydantic import PrivateAttr
|
|
12
|
+
|
|
10
13
|
from .base import DBModelMetaclass, RootModel
|
|
11
14
|
|
|
12
15
|
|
|
@@ -29,6 +32,15 @@ class InstanceId(RootModel):
|
|
|
29
32
|
return (self.space, self.external_id)
|
|
30
33
|
|
|
31
34
|
|
|
35
|
+
class EdgeContainer(InstanceId):
|
|
36
|
+
type: InstanceId
|
|
37
|
+
start_node: InstanceId
|
|
38
|
+
end_node: InstanceId
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
TInstanceId = TypeVar("TInstanceId", bound=InstanceId)
|
|
42
|
+
|
|
43
|
+
|
|
32
44
|
class ViewInstanceConfig(TypedDict, total=False):
|
|
33
45
|
view_external_id: str | None
|
|
34
46
|
instance_spaces: list[str] | None
|
|
@@ -38,12 +50,38 @@ class ViewInstanceConfig(TypedDict, total=False):
|
|
|
38
50
|
class ViewInstance(InstanceId, metaclass=DBModelMetaclass):
|
|
39
51
|
view_config: ClassVar[ViewInstanceConfig] = ViewInstanceConfig()
|
|
40
52
|
|
|
53
|
+
_edges: dict[str, list[EdgeContainer]] = PrivateAttr(default_factory=dict)
|
|
54
|
+
|
|
41
55
|
@classmethod
|
|
42
56
|
def get_view_external_id(cls) -> str:
|
|
43
57
|
return cls.view_config.get("view_external_id") or cls.__name__
|
|
44
58
|
|
|
59
|
+
def get_field_name(self, field_name_or_alias: str) -> str | None:
|
|
60
|
+
entry = self.__class__.model_fields.get(field_name_or_alias)
|
|
61
|
+
if entry:
|
|
62
|
+
return field_name_or_alias
|
|
63
|
+
|
|
64
|
+
for key, field_info in self.__class__.model_fields.items():
|
|
65
|
+
if field_info.alias == field_name_or_alias:
|
|
66
|
+
return key
|
|
67
|
+
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class WritableViewInstance(ViewInstance):
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def edge_id_factory(
|
|
74
|
+
self, target_node: TInstanceId, edge_type: InstanceId
|
|
75
|
+
) -> InstanceId:
|
|
76
|
+
raise NotImplementedError(
|
|
77
|
+
"edge_id_factory method must be implemented in subclasses"
|
|
78
|
+
)
|
|
79
|
+
|
|
45
80
|
|
|
46
81
|
TViewInstance = TypeVar("TViewInstance", bound=ViewInstance)
|
|
82
|
+
TWritableViewInstance = TypeVar(
|
|
83
|
+
"TWritableViewInstance", bound=WritableViewInstance
|
|
84
|
+
)
|
|
47
85
|
|
|
48
86
|
|
|
49
87
|
class PaginatedResult(RootModel, Generic[TViewInstance]):
|
|
@@ -52,4 +90,10 @@ class PaginatedResult(RootModel, Generic[TViewInstance]):
|
|
|
52
90
|
next_cursor: str | None
|
|
53
91
|
|
|
54
92
|
|
|
93
|
+
class AggregationResult(RootModel):
|
|
94
|
+
group: dict[str, str | int | float | bool] | None
|
|
95
|
+
value: float
|
|
96
|
+
aggregate: str
|
|
97
|
+
|
|
98
|
+
|
|
55
99
|
ValidationMode = Literal["raiseOnError", "ignoreOnError"]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from dataclasses import dataclass, field
|
|
2
|
-
from typing import Any, Generic, Self, TypeVar
|
|
2
|
+
from typing import Any, Generic, Literal, Self, TypeVar
|
|
3
3
|
|
|
4
4
|
from industrial_model.constants import DEFAULT_LIMIT, SORT_DIRECTION
|
|
5
5
|
|
|
@@ -17,6 +17,10 @@ from .expressions import (
|
|
|
17
17
|
T = TypeVar("T")
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
def _create_column(property: str | Column | Any) -> Column:
|
|
21
|
+
return property if isinstance(property, Column) else Column(property)
|
|
22
|
+
|
|
23
|
+
|
|
20
24
|
@dataclass
|
|
21
25
|
class Statement(Generic[T]):
|
|
22
26
|
entity: type[T] = field(init=True)
|
|
@@ -33,16 +37,21 @@ class Statement(Generic[T]):
|
|
|
33
37
|
self.where_clauses.append(expression)
|
|
34
38
|
return self
|
|
35
39
|
|
|
36
|
-
def asc(self, property: Any) -> Self:
|
|
37
|
-
self.
|
|
38
|
-
return self
|
|
40
|
+
def asc(self, property: str | Column | Any) -> Self:
|
|
41
|
+
return self.sort(property, "ascending")
|
|
39
42
|
|
|
40
|
-
def desc(self, property: Any) -> Self:
|
|
41
|
-
self.
|
|
42
|
-
return self
|
|
43
|
+
def desc(self, property: str | Column | Any) -> Self:
|
|
44
|
+
return self.sort(property, "descending")
|
|
43
45
|
|
|
44
|
-
def sort(
|
|
45
|
-
self
|
|
46
|
+
def sort(
|
|
47
|
+
self, property: str | Column | Any, direction: SORT_DIRECTION
|
|
48
|
+
) -> Self:
|
|
49
|
+
self.sort_clauses.append(
|
|
50
|
+
(
|
|
51
|
+
_create_column(property),
|
|
52
|
+
direction,
|
|
53
|
+
)
|
|
54
|
+
)
|
|
46
55
|
return self
|
|
47
56
|
|
|
48
57
|
def limit(self, limit: int) -> Self:
|
|
@@ -54,11 +63,52 @@ class Statement(Generic[T]):
|
|
|
54
63
|
return self
|
|
55
64
|
|
|
56
65
|
|
|
66
|
+
@dataclass
|
|
67
|
+
class AggregationStatement(Generic[T]):
|
|
68
|
+
aggregate_: Literal["count"] = field(init=False, default="count")
|
|
69
|
+
aggregation_property: Column = field(
|
|
70
|
+
init=False, default=Column("externalId")
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
entity: type[T] = field(init=True)
|
|
74
|
+
group_by_columns: list[Column] = field(init=False, default_factory=list)
|
|
75
|
+
where_clauses: list[Expression] = field(init=False, default_factory=list)
|
|
76
|
+
limit_: int = field(init=False, default=-1)
|
|
77
|
+
|
|
78
|
+
def aggregate(self, aggregates: Literal["count"]) -> Self:
|
|
79
|
+
self.aggregate_ = aggregates
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
def aggregate_by(self, property: str | Column | Any) -> Self:
|
|
83
|
+
self.aggregation_property = _create_column(property)
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def group_by(self, *property: str | Column | Any) -> Self:
|
|
87
|
+
self.group_by_columns.extend(_create_column(p) for p in property)
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
def where(self, *expressions: bool | Expression) -> Self:
|
|
91
|
+
for expression in expressions:
|
|
92
|
+
assert isinstance(expression, Expression)
|
|
93
|
+
self.where_clauses.append(expression)
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def limit(self, limit: int) -> Self:
|
|
97
|
+
self.limit_ = limit
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
|
|
57
101
|
def select(entity: type[T]) -> Statement[T]:
|
|
58
102
|
return Statement(entity)
|
|
59
103
|
|
|
60
104
|
|
|
105
|
+
def aggregate(entity: type[T]) -> AggregationStatement[T]:
|
|
106
|
+
return AggregationStatement(entity)
|
|
107
|
+
|
|
108
|
+
|
|
61
109
|
__all__ = [
|
|
110
|
+
"aggregate",
|
|
111
|
+
"AggregationStatement",
|
|
62
112
|
"Statement",
|
|
63
113
|
"select",
|
|
64
114
|
"Column",
|
industrial_model/utils.py
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
from collections.abc import Callable
|
|
1
|
+
from collections.abc import Callable
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
from typing import (
|
|
4
|
-
Any,
|
|
5
4
|
ParamSpec,
|
|
6
5
|
TypeVar,
|
|
7
6
|
)
|
|
8
7
|
|
|
9
8
|
from anyio import to_thread
|
|
10
9
|
|
|
11
|
-
TAny = TypeVar("TAny")
|
|
12
10
|
T_Retval = TypeVar("T_Retval")
|
|
13
11
|
P = ParamSpec("P")
|
|
14
12
|
|
|
@@ -21,15 +19,6 @@ def datetime_to_ms_iso_timestamp(dt: datetime) -> str:
|
|
|
21
19
|
return dt.isoformat(timespec="milliseconds")
|
|
22
20
|
|
|
23
21
|
|
|
24
|
-
def chunk_list(
|
|
25
|
-
entries: list[TAny], chunk_size: int
|
|
26
|
-
) -> Generator[list[TAny], Any, None]:
|
|
27
|
-
for i in range(0, len(entries), chunk_size):
|
|
28
|
-
start = i
|
|
29
|
-
end = i + chunk_size
|
|
30
|
-
yield entries[start:end]
|
|
31
|
-
|
|
32
|
-
|
|
33
22
|
async def run_async(
|
|
34
23
|
func: Callable[..., T_Retval],
|
|
35
24
|
*args: object,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: industrial-model
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.11
|
|
4
4
|
Summary: Industrial Model ORM
|
|
5
5
|
Author-email: Lucas Alves <lucasrosaalves@gmail.com>
|
|
6
6
|
Classifier: Programming Language :: Python
|
|
@@ -181,6 +181,40 @@ statement = select(Person).where(
|
|
|
181
181
|
)
|
|
182
182
|
all_results = engine.query_all_pages(statement)
|
|
183
183
|
|
|
184
|
+
|
|
185
|
+
# 7. Data Ingestion
|
|
186
|
+
|
|
187
|
+
from industrial_model import (
|
|
188
|
+
WritableViewInstance # necessary for data ingestion
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class WritablePerson(WritableViewInstance):
|
|
193
|
+
name: str
|
|
194
|
+
lives_in: InstanceId
|
|
195
|
+
cars: list[InstanceId]
|
|
196
|
+
|
|
197
|
+
# You need to implement the end_id_factory so the model can build the edge ids automatically.
|
|
198
|
+
def edge_id_factory(
|
|
199
|
+
self, target_node: InstanceId, edge_type: InstanceId
|
|
200
|
+
) -> InstanceId:
|
|
201
|
+
return InstanceId(
|
|
202
|
+
external_id=f"{self.external_id}-{target_node.external_id}-{edge_type.external_id}",
|
|
203
|
+
space=self.space,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
statement = select(WritablePerson).where(
|
|
207
|
+
(WritablePerson.external_id == "Lucas")
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
person = engine.query_all_pages(statement)[0]
|
|
211
|
+
|
|
212
|
+
person.lives_in = InstanceId(external_id="br", space="data-space")
|
|
213
|
+
person.cars.clear() # Gonna remove all car edges from the person
|
|
214
|
+
|
|
215
|
+
engine.upsert([person])
|
|
216
|
+
|
|
184
217
|
```
|
|
185
218
|
|
|
219
|
+
|
|
186
220
|
---
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
industrial_model/__init__.py,sha256=13TekQQlw7euJr1pIncV1R1Z8cWtvCPPx5e6s0NY_C0,711
|
|
2
|
+
industrial_model/config.py,sha256=bNEfjgD2vth4RPGKDwjGmh0edleQ95g__UJGHV4vix0,520
|
|
3
|
+
industrial_model/constants.py,sha256=wtFdxkR9gojqekwXeyVCof6VUkJYuqjjgAgVVKrcxaw,450
|
|
4
|
+
industrial_model/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
industrial_model/utils.py,sha256=oh4AxwxXaWgIC2uolkCbvkgo0ququHB6yAPVIXy45Ts,663
|
|
6
|
+
industrial_model/cognite_adapters/__init__.py,sha256=a7xPiSkuWkVxvsmVfFD-lOzdOc-tTNo8JmbyL5tC-68,5461
|
|
7
|
+
industrial_model/cognite_adapters/aggregation_mapper.py,sha256=wG8ottGilTAJcpf2kQvhvyRmDBpSKiT4esx0P5kmZBU,1845
|
|
8
|
+
industrial_model/cognite_adapters/filter_mapper.py,sha256=NqH-OW7_iKFY9POCG8W3KjkwXUgrZP1d_yxDx1J0fXM,3859
|
|
9
|
+
industrial_model/cognite_adapters/models.py,sha256=2j2IS01uPkQEp9WdVk8seYzEqGcDdWFnpzXhusHB2zk,945
|
|
10
|
+
industrial_model/cognite_adapters/optimizer.py,sha256=BbbsA3r9ZKJt6mN1wVRZc046ov-vbml9_ESfurTNRUg,2477
|
|
11
|
+
industrial_model/cognite_adapters/query_mapper.py,sha256=3fEcaLsGjLKIh-g1BbMcffQ6rp99JeCW555iJo8JW44,6260
|
|
12
|
+
industrial_model/cognite_adapters/query_result_mapper.py,sha256=lsKz0wiqMOo54MVEw_YqLcHXa_HWYgAbSLnBbbdVupw,8451
|
|
13
|
+
industrial_model/cognite_adapters/sort_mapper.py,sha256=RJUAYlZGXoYzK0PwX63cibRF_L-MUq9g2ZsC2EeNIF4,696
|
|
14
|
+
industrial_model/cognite_adapters/upsert_mapper.py,sha256=tWEiBJQeeNz1HDu0AoBIfCw_LL156Zg4h7ORKlZ__uw,4870
|
|
15
|
+
industrial_model/cognite_adapters/utils.py,sha256=rztCtS10ZTQmXfBv0nLgDiQIMWemhdSFK-SwrbRjVxM,4792
|
|
16
|
+
industrial_model/cognite_adapters/view_mapper.py,sha256=qvT25gwIgPdAqZf-oQEh0GylUi5KEPwjLNmIl5ISQGA,1474
|
|
17
|
+
industrial_model/engines/__init__.py,sha256=7aGHrUm2MxIq39vR8h0xu3i1zNOuT9H9U-q4lV3nErQ,102
|
|
18
|
+
industrial_model/engines/async_engine.py,sha256=qILOHTBnq8DOwHoGnBQ6qrRh2zv2-0rjBd36hqWXM0w,1523
|
|
19
|
+
industrial_model/engines/engine.py,sha256=fSJL0i7TwJsVP_-uNrN_WoxToCpwidIik58KREEw-oQ,3003
|
|
20
|
+
industrial_model/models/__init__.py,sha256=klwPCssjGx-0rGi8aBLzP6GWO9kgR7zDAo93el9hQ1I,672
|
|
21
|
+
industrial_model/models/base.py,sha256=jbiaICJ0R1mmxXtDqxxlVdq-tTX4RLdqnLTgs9HLm_4,1279
|
|
22
|
+
industrial_model/models/entities.py,sha256=x4c-ch-9cRVVk039dx60kXSDFhsxQ3lIY96tJ4vPjYI,2506
|
|
23
|
+
industrial_model/models/schemas.py,sha256=LKNPDnUy1jtMyOHDf28En9vThhdzOSswewIcjC_y-6U,4393
|
|
24
|
+
industrial_model/queries/__init__.py,sha256=7aheTE5qs03rxWm9fmGWptbz_p9OIXXYD8if56cqs18,227
|
|
25
|
+
industrial_model/queries/models.py,sha256=iiHQ7-cfg0nukEv5PoCx9QPF-w1gVSnoNbXBOK9Mzeo,1185
|
|
26
|
+
industrial_model/queries/params.py,sha256=ehgCoR5n6E-tkEuoymZ2lkLcSzMaBAx_HnyJ7sWpqz0,964
|
|
27
|
+
industrial_model/statements/__init__.py,sha256=Y57G1TJlYxCHg3Zz7ojQoAMVnCMaUCRRWXEm2IcbAWo,3332
|
|
28
|
+
industrial_model/statements/expressions.py,sha256=Sar1cIvy3sYi7tkWJN3ylHlZ252oN2mZJpZ1TX9jN3s,4940
|
|
29
|
+
industrial_model-0.1.11.dist-info/METADATA,sha256=IXdJyysssgu2JDxbSe6IN0Wg3US7Ax3XYu4NXUXU838,5698
|
|
30
|
+
industrial_model-0.1.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
31
|
+
industrial_model-0.1.11.dist-info/RECORD,,
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
industrial_model/__init__.py,sha256=qst1HvSXw7ytYTvi_hgg8989rSqKXF3n_8jv3wO73x8,525
|
|
2
|
-
industrial_model/config.py,sha256=wzSKVKHIdGlxCRtu0PIeL3SLYvnQBR4Py46CcETxa8U,238
|
|
3
|
-
industrial_model/constants.py,sha256=wtFdxkR9gojqekwXeyVCof6VUkJYuqjjgAgVVKrcxaw,450
|
|
4
|
-
industrial_model/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
industrial_model/utils.py,sha256=OuuAkIqu-551axsZJPuqHu5d9iReGcfY9ROBFtHIwkY,933
|
|
6
|
-
industrial_model/cognite_adapters/__init__.py,sha256=SjI-cO8tsZpd1uBSyP6nt3WvWbMHnWzf3v1X5oDTNvw,3436
|
|
7
|
-
industrial_model/cognite_adapters/filter_mapper.py,sha256=NqH-OW7_iKFY9POCG8W3KjkwXUgrZP1d_yxDx1J0fXM,3859
|
|
8
|
-
industrial_model/cognite_adapters/optimizer.py,sha256=G8I07jJ9tarE5GZXXUSpTMKUei6ptV-cudJSLsoykX4,2223
|
|
9
|
-
industrial_model/cognite_adapters/query_mapper.py,sha256=3fEcaLsGjLKIh-g1BbMcffQ6rp99JeCW555iJo8JW44,6260
|
|
10
|
-
industrial_model/cognite_adapters/query_result_mapper.py,sha256=cDQ2eEOqKRr4Ifowal7qvUlKSDLUj-xMFSo6qcR_UPE,7179
|
|
11
|
-
industrial_model/cognite_adapters/sort_mapper.py,sha256=RJUAYlZGXoYzK0PwX63cibRF_L-MUq9g2ZsC2EeNIF4,696
|
|
12
|
-
industrial_model/cognite_adapters/utils.py,sha256=rztCtS10ZTQmXfBv0nLgDiQIMWemhdSFK-SwrbRjVxM,4792
|
|
13
|
-
industrial_model/cognite_adapters/view_mapper.py,sha256=lnv64KezSQTAr6XdcExa8d92GU5Ll9K9HfMcQGzhX6k,488
|
|
14
|
-
industrial_model/engines/__init__.py,sha256=7aGHrUm2MxIq39vR8h0xu3i1zNOuT9H9U-q4lV3nErQ,102
|
|
15
|
-
industrial_model/engines/async_engine.py,sha256=3Y4Ao14CDJJAZFbgTX9I6LOJUxfe8nwOOukGVeiw914,1070
|
|
16
|
-
industrial_model/engines/engine.py,sha256=8-wfktRLZpad_V7_F1Vz7vtjE_bQhga_jXXjuwSgfrE,1897
|
|
17
|
-
industrial_model/models/__init__.py,sha256=q2cI-WutiT-gmRlwC5CnacWqtQo0oSIr7Ohn9hK9jes,474
|
|
18
|
-
industrial_model/models/base.py,sha256=jbiaICJ0R1mmxXtDqxxlVdq-tTX4RLdqnLTgs9HLm_4,1279
|
|
19
|
-
industrial_model/models/entities.py,sha256=NrFV_a3ZP6Y4b1M2PFl_746qcCsexsyO2cuB0snZhkw,1319
|
|
20
|
-
industrial_model/models/schemas.py,sha256=LKNPDnUy1jtMyOHDf28En9vThhdzOSswewIcjC_y-6U,4393
|
|
21
|
-
industrial_model/queries/__init__.py,sha256=7aheTE5qs03rxWm9fmGWptbz_p9OIXXYD8if56cqs18,227
|
|
22
|
-
industrial_model/queries/models.py,sha256=iiHQ7-cfg0nukEv5PoCx9QPF-w1gVSnoNbXBOK9Mzeo,1185
|
|
23
|
-
industrial_model/queries/params.py,sha256=ehgCoR5n6E-tkEuoymZ2lkLcSzMaBAx_HnyJ7sWpqz0,964
|
|
24
|
-
industrial_model/statements/__init__.py,sha256=9fD-qpNXIkrjoahxC_R6hS4DKSVelehimvRPKbpYfA0,1775
|
|
25
|
-
industrial_model/statements/expressions.py,sha256=Sar1cIvy3sYi7tkWJN3ylHlZ252oN2mZJpZ1TX9jN3s,4940
|
|
26
|
-
industrial_model-0.1.9.dist-info/METADATA,sha256=E1MZ0OJAQiRttTC-Dea6eEthmVdj_N0bLYQ7ccNSWXI,4806
|
|
27
|
-
industrial_model-0.1.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
28
|
-
industrial_model-0.1.9.dist-info/RECORD,,
|
|
File without changes
|