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.
@@ -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 TViewInstance
15
- from industrial_model.statements import Statement
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
- dm = cognite_client.data_modeling.data_models.retrieve(
36
- ids=data_model_id.as_tuple(),
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._optmizer = QueryOptimizer(cognite_client)
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: list[str] | None = None
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._get_all_spaces()
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 _get_all_spaces(self) -> list[str]:
72
+ def _load_spaces(self) -> list[str]:
70
73
  all_spaces = self._all_spaces
71
- if all_spaces is None:
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
- self._all_spaces = all_spaces
77
- return all_spaces
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, (values, is_list) in mappings.items():
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
- entries = values.get(element_key)
93
- if not entries:
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(entries)
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, tuple[dict[tuple[str, str], list[Node]], bool]]:
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
- entry: dict[tuple[str, str], list[Node]] | None = None
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
- entry = self._map_node_property(
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
- entry = self._map_node_property(
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
- entry = self._map_node_property(
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
- entry = self._map_edge_property(
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 entry:
163
- mappings[property_name] = entry, is_list
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
- ) -> dict[tuple[str, str], list[Node]] | None:
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
- result: defaultdict[tuple[str, str], list[Node]] = defaultdict(list)
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
- result[entry_key].extend(node_item)
227
+ nodes_result[entry_key].extend(node_item)
201
228
 
202
- return dict(result)
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__(self, views: list[View]):
8
- self._views_as_dict = {view.external_id: view for view in views}
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
- if view_external_id not in self._views_as_dict:
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 self._views_as_dict[view_external_id]
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
@@ -1,10 +1,19 @@
1
- from industrial_model.models import RootModel
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
- result.append(entity.model_validate(item))
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.sort_clauses.append((Column(property), "ascending"))
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.sort_clauses.append((Column(property), "descending"))
42
- return self
43
+ def desc(self, property: str | Column | Any) -> Self:
44
+ return self.sort(property, "descending")
43
45
 
44
- def sort(self, property: Any, direction: SORT_DIRECTION) -> Self:
45
- self.sort_clauses.append((Column(property), direction))
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, Generator
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.9
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,,