industrial-model 1.2.1__py3-none-any.whl → 1.2.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.
@@ -0,0 +1,1118 @@
1
+ Metadata-Version: 2.4
2
+ Name: industrial-model
3
+ Version: 1.2.3
4
+ Summary: Industrial Model ORM
5
+ Project-URL: Homepage, https://github.com/lucasrosaalves/industrial-model
6
+ Project-URL: Source, https://github.com/lucasrosaalves/industrial-model
7
+ Author-email: Lucas Alves <lucasrosaalves@gmail.com>
8
+ License-File: LICENSE
9
+ Classifier: Programming Language :: Python
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Database
16
+ Classifier: Topic :: Database :: Database Engines/Servers
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: anyio>=4.9.0
20
+ Requires-Dist: cognite-sdk>=7.87.0
21
+ Requires-Dist: pydantic>=2.11.4
22
+ Requires-Dist: pyyaml>=6.0.2
23
+ Description-Content-Type: text/markdown
24
+
25
+ # 📦 industrial-model
26
+
27
+ **Type-safe, Pythonic access to Cognite Data Fusion views.**
28
+
29
+ `industrial-model` is a Python ORM for Cognite Data Fusion (CDF). Define views as Pydantic models, build queries with a fluent API, and work with CDF in the same way you write the rest of your Python—with types, autocomplete, and clear errors.
30
+
31
+ ```python
32
+ from industrial_model import Engine, ViewInstance, ViewInstanceConfig, select
33
+ from pathlib import Path
34
+
35
+ class Asset(ViewInstance):
36
+ view_config = ViewInstanceConfig(
37
+ instance_spaces_prefix="instace_data-", # Define the scope of your instance spaces to improve performance.
38
+ )
39
+ name: str
40
+ description: str | None = None
41
+
42
+ engine = Engine.from_config_file(Path("cognite-sdk-config.yaml"))
43
+ results = engine.query(select(Asset).limit(10))
44
+ # results.data → list[Asset], fully typed
45
+ ```
46
+
47
+ ---
48
+
49
+ ## ✨ Features
50
+
51
+ - **Declarative models** — Pydantic-style classes with type hints; only the fields you need
52
+ - **Type-safe queries** — Fluent, composable filters with full IDE support
53
+ - **Query, search, aggregate** — Standard and paginated queries, full-text search, count/sum/avg/min/max
54
+ - **Rich filtering** — Nested queries, edge filters, boolean logic, list/string operators
55
+ - **Read and write** — Upsert and delete with edge relationship support
56
+ - **Async** — All operations have async equivalents
57
+ - **Configurable validation** — Choose how to handle validation errors per request
58
+
59
+ ---
60
+
61
+ ## 📦 Installation
62
+
63
+ ```bash
64
+ pip install industrial-model
65
+ ```
66
+
67
+ ---
68
+
69
+ ## 📚 Table of Contents
70
+
71
+ | Section | What you'll find |
72
+ |--------|-------------------|
73
+ | [Getting Started](#-getting-started) | Prerequisites and example schema |
74
+ | [Model Definition](#-model-definition) | Views as Pydantic models, aliases, config |
75
+ | [Engine Setup](#-engine-setup) | Config file or manual `Engine` / `AsyncEngine` |
76
+ | [Querying Data](#-querying-data) | `select()`, pagination, sorting, validation |
77
+ | [Filtering](#-filtering) | Comparison, list, string, nested, and edge filters |
78
+ | [Search](#-search) | Full-text search with filters |
79
+ | [Aggregations](#-aggregations) | Count, sum, avg, min, max, with grouping |
80
+ | [Write Operations](#-write-operations) | Upsert and delete |
81
+ | [Advanced Features](#-advanced-features) | ID generation, `InstanceId`, result helpers |
82
+ | [Async Operations](#-async-operations) | `AsyncEngine` and async API |
83
+
84
+ ---
85
+
86
+ ## 🚀 Getting Started
87
+
88
+ The quick example above shows the core flow: **model → engine → query**. The rest of this guide uses the `CogniteAsset` view from the `CogniteCore` data model (version `v1`).
89
+
90
+ ### Sample GraphQL Schema
91
+
92
+ ```graphql
93
+ type CogniteAsset {
94
+ name: String
95
+ description: String
96
+ tags: [String]
97
+ aliases: [String]
98
+ parent: CogniteAsset
99
+ root: CogniteAsset
100
+ }
101
+ ```
102
+
103
+ ---
104
+
105
+ ## 🏗️ Model Definition
106
+
107
+ Models map CDF views to Python classes. Inherit from `ViewInstance` (or `WritableViewInstance` for writes) and declare only the properties you need.
108
+
109
+ ### Basic Model
110
+
111
+ Define your model by inheriting from `ViewInstance` and adding only the properties you need:
112
+
113
+ ```python
114
+ from industrial_model import ViewInstance
115
+
116
+ class CogniteAsset(ViewInstance):
117
+ name: str
118
+ description: str
119
+ aliases: list[str]
120
+ ```
121
+
122
+ ### Model with Relationships
123
+
124
+ Include nested relationships by referencing other models:
125
+
126
+ ```python
127
+ from industrial_model import ViewInstance
128
+
129
+ class CogniteAsset(ViewInstance):
130
+ name: str
131
+ description: str
132
+ aliases: list[str]
133
+ parent: CogniteAsset | None = None
134
+ root: CogniteAsset | None = None
135
+ ```
136
+
137
+ ### Field Aliases
138
+
139
+ Use Pydantic's `Field` to map properties to different names in CDF:
140
+
141
+ ```python
142
+ from pydantic import Field
143
+ from industrial_model import ViewInstance
144
+
145
+ class CogniteAsset(ViewInstance):
146
+ asset_name: str = Field(alias="name") # Maps to "name" in CDF
147
+ asset_description: str = Field(alias="description")
148
+ ```
149
+
150
+ ### View Configuration
151
+
152
+ Configure view mapping and space filtering:
153
+
154
+ ```python
155
+ from industrial_model import ViewInstance, ViewInstanceConfig
156
+
157
+ class CogniteAsset(ViewInstance):
158
+ view_config = ViewInstanceConfig(
159
+ view_external_id="CogniteAsset", # Maps this class to the 'CogniteAsset' view
160
+ instance_spaces_prefix="Industr-", # Filters queries to spaces with this prefix
161
+ # OR use explicit spaces:
162
+ # instance_spaces=["Industrial-Data", "Industrial-Production"],
163
+ view_code="ASSET", # Optional: prefix for ID generation
164
+ )
165
+ name: str
166
+ description: str
167
+ aliases: list[str]
168
+ ```
169
+
170
+ ### Writable Models
171
+
172
+ For write operations, inherit from `WritableViewInstance` and implement `edge_id_factory`:
173
+
174
+ ```python
175
+ from industrial_model import WritableViewInstance, InstanceId, ViewInstanceConfig
176
+
177
+ class CogniteAsset(WritableViewInstance):
178
+ view_config = ViewInstanceConfig(view_external_id="CogniteAsset")
179
+ name: str
180
+ aliases: list[str]
181
+ parent: CogniteAsset | None = None
182
+
183
+ def edge_id_factory(self, target_node: InstanceId, edge_type: InstanceId) -> InstanceId:
184
+ """Generate edge IDs for relationships."""
185
+ return InstanceId(
186
+ external_id=f"{self.external_id}-{target_node.external_id}-{edge_type.external_id}",
187
+ space=self.space,
188
+ )
189
+ ```
190
+
191
+ ### Aggregated Models
192
+
193
+ For aggregation queries, use `AggregatedViewInstance`:
194
+
195
+ ```python
196
+ from industrial_model import AggregatedViewInstance, ViewInstanceConfig
197
+
198
+ class CogniteAssetByName(AggregatedViewInstance):
199
+ view_config = ViewInstanceConfig(view_external_id="CogniteAsset")
200
+ name: str
201
+ # The 'value' field is automatically included for aggregation results
202
+ ```
203
+
204
+ ---
205
+
206
+ ## ⚙️ Engine Setup
207
+
208
+ The engine connects to CDF and knows which data model and version to use. You can load it from a config file or build it from an existing `CogniteClient`.
209
+
210
+ ### Option A: From Configuration File
211
+
212
+ Create a `cognite-sdk-config.yaml` file:
213
+
214
+ ```yaml
215
+ cognite:
216
+ project: "${CDF_PROJECT}"
217
+ client_name: "${CDF_CLIENT_NAME}"
218
+ base_url: "https://${CDF_CLUSTER}.cognitedata.com"
219
+ credentials:
220
+ client_credentials:
221
+ token_url: "${CDF_TOKEN_URL}"
222
+ client_id: "${CDF_CLIENT_ID}"
223
+ client_secret: "${CDF_CLIENT_SECRET}"
224
+ scopes: ["https://${CDF_CLUSTER}.cognitedata.com/.default"]
225
+
226
+ data_model:
227
+ external_id: "CogniteCore"
228
+ space: "cdf_cdm"
229
+ version: "v1"
230
+ ```
231
+
232
+ ```python
233
+ from industrial_model import Engine
234
+ from pathlib import Path
235
+
236
+ engine = Engine.from_config_file(Path("cognite-sdk-config.yaml"))
237
+ ```
238
+
239
+ ### Option B: Manual Setup
240
+
241
+ ```python
242
+ from cognite.client import CogniteClient
243
+ from industrial_model import Engine, DataModelId
244
+
245
+ # Create your CogniteClient with appropriate authentication
246
+ cognite_client = CogniteClient(
247
+ # ... your client configuration
248
+ )
249
+
250
+ engine = Engine(
251
+ cognite_client=cognite_client,
252
+ data_model_id=DataModelId(
253
+ external_id="CogniteCore",
254
+ space="cdf_cdm",
255
+ version="v1"
256
+ )
257
+ )
258
+ ```
259
+
260
+ ### Async Engine
261
+
262
+ For async operations, use `AsyncEngine`:
263
+
264
+ ```python
265
+ from industrial_model import AsyncEngine
266
+ from pathlib import Path
267
+
268
+ async_engine = AsyncEngine.from_config_file(Path("cognite-sdk-config.yaml"))
269
+ ```
270
+
271
+ ---
272
+
273
+ ## 🔎 Querying Data
274
+
275
+ Use `select()` to build statements, then run them with `engine.query()` or `engine.query_all_pages()`. Results are typed and paginated.
276
+
277
+ ### Basic Query
278
+
279
+ ```python
280
+ from industrial_model import select
281
+
282
+ statement = select(CogniteAsset).limit(100)
283
+ results = engine.query(statement)
284
+
285
+ # results is a PaginatedResult with:
286
+ # - results.data: list of instances
287
+ # - results.has_next_page: bool
288
+ # - results.next_cursor: str | None
289
+ ```
290
+
291
+ ### Query All Pages
292
+
293
+ Fetch all results across multiple pages:
294
+
295
+ ```python
296
+ statement = select(CogniteAsset).limit(1000)
297
+ all_results = engine.query_all_pages(statement) # Returns list[TViewInstance]
298
+ ```
299
+
300
+ ### Pagination with Cursor
301
+
302
+ ```python
303
+ # First page
304
+ statement = select(CogniteAsset).limit(100)
305
+ page1 = engine.query(statement)
306
+
307
+ # Next page using cursor
308
+ if page1.has_next_page:
309
+ statement = select(CogniteAsset).limit(100).cursor(page1.next_cursor)
310
+ page2 = engine.query(statement)
311
+ ```
312
+
313
+ ### Sorting
314
+
315
+ ```python
316
+ from industrial_model import select
317
+
318
+ # Ascending order
319
+ statement = select(CogniteAsset).asc(CogniteAsset.name)
320
+
321
+ # Descending order
322
+ statement = select(CogniteAsset).desc(CogniteAsset.name)
323
+
324
+ # Multiple sort fields
325
+ statement = (
326
+ select(CogniteAsset)
327
+ .asc(CogniteAsset.name)
328
+ .desc(CogniteAsset.external_id)
329
+ )
330
+ ```
331
+
332
+ ### Validation Modes
333
+
334
+ Control how validation errors are handled:
335
+
336
+ ```python
337
+ # Raise on error (default)
338
+ results = engine.query(statement, validation_mode="raiseOnError")
339
+
340
+ # Ignore validation errors
341
+ results = engine.query(statement, validation_mode="ignoreOnError")
342
+ ```
343
+
344
+ ---
345
+
346
+ ## 🔍 Filtering
347
+
348
+ Add `.where(...)` to narrow results. Use `col()` for operators like `in_()`, `prefix()`, and `nested_()`; use `==`, `!=`, `&`, and `|` where they apply.
349
+
350
+ ### Comparison Operators
351
+
352
+ ```python
353
+ from industrial_model import select, col
354
+
355
+ # Equality
356
+ statement = select(CogniteAsset).where(CogniteAsset.name == "My Asset")
357
+ # or
358
+ statement = select(CogniteAsset).where(col(CogniteAsset.name).equals_("My Asset"))
359
+
360
+ # Inequality
361
+ statement = select(CogniteAsset).where(CogniteAsset.name != "My Asset")
362
+
363
+ # Less than / Less than or equal
364
+ statement = select(CogniteAsset).where(col(CogniteAsset.external_id).lt_("Z"))
365
+ statement = select(CogniteAsset).where(col(CogniteAsset.external_id).lte_("Z"))
366
+
367
+ # Greater than / Greater than or equal
368
+ statement = select(CogniteAsset).where(col(CogniteAsset.external_id).gt_("A"))
369
+ statement = select(CogniteAsset).where(col(CogniteAsset.external_id).gte_("A"))
370
+ ```
371
+
372
+ ### List Operators
373
+
374
+ ```python
375
+ from industrial_model import select, col
376
+
377
+ # In (matches any value in list)
378
+ statement = select(CogniteAsset).where(
379
+ col(CogniteAsset.external_id).in_(["asset-1", "asset-2", "asset-3"])
380
+ )
381
+
382
+ # Contains any (for array fields)
383
+ statement = select(CogniteAsset).where(
384
+ col(CogniteAsset.aliases).contains_any_(["alias1", "alias2"])
385
+ )
386
+
387
+ # Contains all (for array fields)
388
+ statement = select(CogniteAsset).where(
389
+ col(CogniteAsset.tags).contains_all_(["tag1", "tag2"])
390
+ )
391
+ ```
392
+
393
+ ### String Operators
394
+
395
+ ```python
396
+ from industrial_model import select, col
397
+
398
+ # Prefix matching
399
+ statement = select(CogniteAsset).where(
400
+ col(CogniteAsset.name).prefix("Pump-")
401
+ )
402
+ ```
403
+
404
+ ### Existence Operators
405
+
406
+ ```python
407
+ from industrial_model import select, col
408
+
409
+ # Field exists
410
+ statement = select(CogniteAsset).where(
411
+ col(CogniteAsset.description).exists_()
412
+ )
413
+
414
+ # Field does not exist
415
+ statement = select(CogniteAsset).where(
416
+ col(CogniteAsset.description).not_exists_()
417
+ )
418
+
419
+ # Using == and != with None
420
+ statement = select(CogniteAsset).where(
421
+ CogniteAsset.parent == None # Field is null
422
+ )
423
+ statement = select(CogniteAsset).where(
424
+ CogniteAsset.parent != None # Field is not null
425
+ )
426
+ ```
427
+
428
+ ### Nested Queries
429
+
430
+ Filter by properties of related instances:
431
+
432
+ ```python
433
+ from industrial_model import select, col
434
+
435
+ # Filter by parent's name
436
+ statement = select(CogniteAsset).where(
437
+ col(CogniteAsset.parent).nested_(
438
+ col(CogniteAsset.name) == "Parent Asset Name"
439
+ )
440
+ )
441
+
442
+ # Multiple nested conditions
443
+ statement = select(CogniteAsset).where(
444
+ col(CogniteAsset.parent).nested_(
445
+ (col(CogniteAsset.name) == "Parent Asset") &
446
+ (col(CogniteAsset.external_id).prefix("PARENT-"))
447
+ )
448
+ )
449
+ ```
450
+
451
+ ### Boolean Operators
452
+
453
+ Combine filters using `&`, `|`, and boolean functions:
454
+
455
+ ```python
456
+ from industrial_model import select, col, and_, or_, not_
457
+
458
+ # Using & (AND) operator
459
+ statement = select(CogniteAsset).where(
460
+ (col(CogniteAsset.name).prefix("Pump-")) &
461
+ (col(CogniteAsset.aliases).contains_any_(["pump"]))
462
+ )
463
+
464
+ # Using | (OR) operator
465
+ statement = select(CogniteAsset).where(
466
+ (col(CogniteAsset.name) == "Asset 1") |
467
+ (col(CogniteAsset.name) == "Asset 2")
468
+ )
469
+
470
+ # Using and_() function
471
+ statement = select(CogniteAsset).where(
472
+ and_(
473
+ col(CogniteAsset.aliases).contains_any_(["my_alias"]),
474
+ col(CogniteAsset.description).exists_(),
475
+ )
476
+ )
477
+
478
+ # Using or_() function
479
+ statement = select(CogniteAsset).where(
480
+ or_(
481
+ col(CogniteAsset.name) == "Asset 1",
482
+ col(CogniteAsset.name) == "Asset 2",
483
+ col(CogniteAsset.name) == "Asset 3",
484
+ )
485
+ )
486
+
487
+ # Using not_() function
488
+ statement = select(CogniteAsset).where(
489
+ not_(col(CogniteAsset.name).prefix("Test-"))
490
+ )
491
+
492
+ # Complex combinations
493
+ statement = select(CogniteAsset).where(
494
+ and_(
495
+ col(CogniteAsset.aliases).contains_any_(["my_alias"]),
496
+ or_(
497
+ col(CogniteAsset.parent).nested_(
498
+ col(CogniteAsset.name) == "Parent Asset Name 1"
499
+ ),
500
+ col(CogniteAsset.parent).nested_(
501
+ col(CogniteAsset.name) == "Parent Asset Name 2"
502
+ ),
503
+ ),
504
+ )
505
+ )
506
+ ```
507
+
508
+ ### Edge Filtering
509
+
510
+ Filter on edge properties using `where_edge`:
511
+
512
+ ```python
513
+ from industrial_model import select, col
514
+
515
+ # Filter by edge properties
516
+ statement = (
517
+ select(CogniteAsset)
518
+ .where_edge(
519
+ CogniteAsset.parent,
520
+ col(CogniteAsset.external_id) == "PARENT-123"
521
+ )
522
+ .limit(100)
523
+ )
524
+ ```
525
+
526
+ ### Date/Time Filtering
527
+
528
+ ```python
529
+ from datetime import datetime
530
+ from industrial_model import select, col
531
+
532
+ # Filter by datetime
533
+ cutoff_date = datetime(2024, 1, 1)
534
+ statement = select(CogniteAsset).where(
535
+ col(CogniteAsset.created_time).gte_(cutoff_date)
536
+ )
537
+ ```
538
+
539
+ ### InstanceId Filtering
540
+
541
+ Filter using InstanceId objects:
542
+
543
+ ```python
544
+ from industrial_model import select, col, InstanceId
545
+
546
+ parent_id = InstanceId(external_id="PARENT-123", space="cdf_cdm")
547
+ statement = select(CogniteAsset).where(
548
+ col(CogniteAsset.parent) == parent_id
549
+ )
550
+
551
+ # Or using nested queries
552
+ statement = select(CogniteAsset).where(
553
+ col(CogniteAsset.parent).nested_(
554
+ col(CogniteAsset.external_id) == "PARENT-123"
555
+ )
556
+ )
557
+ ```
558
+
559
+ ---
560
+
561
+ ## 🔍 Search
562
+
563
+ Full-text search over view instances. Combine `search()` with `.where()` filters and `.query_by()` to search specific properties.
564
+
565
+ ### Search with Filters
566
+
567
+ ```python
568
+ from industrial_model import search, col
569
+
570
+ search_statement = (
571
+ search(CogniteAsset)
572
+ .where(col(CogniteAsset.aliases).contains_any_(["my_alias"]))
573
+ .query_by(
574
+ query="pump equipment",
575
+ query_properties=[CogniteAsset.name, CogniteAsset.description],
576
+ )
577
+ )
578
+
579
+ results = engine.search(search_statement)
580
+ ```
581
+
582
+ ### Search Operators
583
+
584
+ ```python
585
+ from industrial_model import search, col
586
+
587
+ # AND operator (all terms must match)
588
+ search_statement = (
589
+ search(CogniteAsset)
590
+ .query_by(
591
+ query="pump equipment",
592
+ query_properties=[CogniteAsset.name],
593
+ operation="AND",
594
+ )
595
+ )
596
+
597
+ # OR operator (any term can match) - default
598
+ search_statement = (
599
+ search(CogniteAsset)
600
+ .query_by(
601
+ query="pump equipment",
602
+ query_properties=[CogniteAsset.name],
603
+ operation="OR",
604
+ )
605
+ )
606
+ ```
607
+
608
+ ### Search with Multiple Properties
609
+
610
+ ```python
611
+ from industrial_model import search, col
612
+
613
+ search_statement = (
614
+ search(CogniteAsset)
615
+ .query_by(
616
+ query="industrial pump",
617
+ query_properties=[
618
+ CogniteAsset.name,
619
+ CogniteAsset.description,
620
+ CogniteAsset.external_id,
621
+ ],
622
+ operation="AND",
623
+ )
624
+ .limit(50)
625
+ )
626
+
627
+ results = engine.search(search_statement)
628
+ ```
629
+
630
+ ---
631
+
632
+ ## 📊 Aggregations
633
+
634
+ Use `AggregatedViewInstance` and `aggregate()` for count, sum, avg, min, and max—optionally with `.group_by()` and `.where()`.
635
+
636
+ ### Count Aggregation
637
+
638
+ ```python
639
+ from industrial_model import aggregate, AggregatedViewInstance, ViewInstanceConfig, col
640
+
641
+ class CogniteAssetCount(AggregatedViewInstance):
642
+ view_config = ViewInstanceConfig(view_external_id="CogniteAsset")
643
+
644
+ # Simple count
645
+ statement = aggregate(CogniteAssetCount, "count")
646
+ results = engine.aggregate(statement)
647
+ # Each result has a 'value' field with the count
648
+
649
+ # Count with grouping
650
+ class CogniteAssetByName(AggregatedViewInstance):
651
+ view_config = ViewInstanceConfig(view_external_id="CogniteAsset")
652
+ name: str
653
+
654
+ statement = aggregate(CogniteAssetByName, "count").group_by(
655
+ col(CogniteAssetByName.name)
656
+ )
657
+ results = engine.aggregate(statement)
658
+ # Results grouped by name, each with a count value
659
+ ```
660
+
661
+ ### Sum Aggregation
662
+
663
+ ```python
664
+ from industrial_model import aggregate, AggregatedViewInstance, ViewInstanceConfig, col
665
+
666
+ class CogniteAssetWithValue(AggregatedViewInstance):
667
+ view_config = ViewInstanceConfig(view_external_id="CogniteAsset")
668
+ name: str
669
+ # Assume there's a 'value' property in the view
670
+
671
+ statement = (
672
+ aggregate(CogniteAssetWithValue, "sum")
673
+ .aggregate_by(CogniteAssetWithValue.value)
674
+ .group_by(col(CogniteAssetWithValue.name))
675
+ )
676
+ results = engine.aggregate(statement)
677
+ ```
678
+
679
+ ### Average, Min, Max Aggregations
680
+
681
+ ```python
682
+ from industrial_model import aggregate, AggregatedViewInstance, ViewInstanceConfig, col
683
+
684
+ class CogniteAssetStats(AggregatedViewInstance):
685
+ view_config = ViewInstanceConfig(view_external_id="CogniteAsset")
686
+ name: str
687
+
688
+ # Average
689
+ statement = (
690
+ aggregate(CogniteAssetStats, "avg")
691
+ .aggregate_by(CogniteAssetStats.value)
692
+ .group_by(col(CogniteAssetStats.name))
693
+ )
694
+
695
+ # Minimum
696
+ statement = (
697
+ aggregate(CogniteAssetStats, "min")
698
+ .aggregate_by(CogniteAssetStats.value)
699
+ .group_by(col(CogniteAssetStats.name))
700
+ )
701
+
702
+ # Maximum
703
+ statement = (
704
+ aggregate(CogniteAssetStats, "max")
705
+ .aggregate_by(CogniteAssetStats.value)
706
+ .group_by(col(CogniteAssetStats.name))
707
+ )
708
+ ```
709
+
710
+ ### Aggregation with Filters
711
+
712
+ ```python
713
+ from industrial_model import aggregate, AggregatedViewInstance, ViewInstanceConfig, col
714
+
715
+ class CogniteAssetByName(AggregatedViewInstance):
716
+ view_config = ViewInstanceConfig(view_external_id="CogniteAsset")
717
+ name: str
718
+
719
+ statement = (
720
+ aggregate(CogniteAssetByName, "count")
721
+ .where(col("description").exists_())
722
+ .group_by(col(CogniteAssetByName.name))
723
+ .limit(100)
724
+ )
725
+
726
+ results = engine.aggregate(statement)
727
+ ```
728
+
729
+ ### Multiple Group By Fields
730
+
731
+ ```python
732
+ from industrial_model import aggregate, AggregatedViewInstance, ViewInstanceConfig, col
733
+
734
+ class CogniteAssetGrouped(AggregatedViewInstance):
735
+ view_config = ViewInstanceConfig(view_external_id="CogniteAsset")
736
+ name: str
737
+ space: str
738
+
739
+ statement = (
740
+ aggregate(CogniteAssetGrouped, "count")
741
+ .group_by(
742
+ col(CogniteAssetGrouped.name),
743
+ col(CogniteAssetGrouped.space),
744
+ )
745
+ )
746
+
747
+ results = engine.aggregate(statement)
748
+ ```
749
+
750
+ ---
751
+
752
+ ## ✏️ Write Operations
753
+
754
+ Use `WritableViewInstance` and implement `edge_id_factory` for models with relationships. Then `engine.upsert()` and `engine.delete()` work on lists of instances.
755
+
756
+ ### Upsert Instances
757
+
758
+ ```python
759
+ from industrial_model import WritableViewInstance, InstanceId, ViewInstanceConfig, select, col
760
+
761
+ class CogniteAsset(WritableViewInstance):
762
+ view_config = ViewInstanceConfig(view_external_id="CogniteAsset")
763
+ name: str
764
+ aliases: list[str]
765
+ parent: CogniteAsset | None = None
766
+
767
+ def edge_id_factory(self, target_node: InstanceId, edge_type: InstanceId) -> InstanceId:
768
+ return InstanceId(
769
+ external_id=f"{self.external_id}-{target_node.external_id}-{edge_type.external_id}",
770
+ space=self.space,
771
+ )
772
+
773
+ # Update existing instances
774
+ instances = engine.query_all_pages(
775
+ select(CogniteAsset).where(col(CogniteAsset.aliases).contains_any_(["my_alias"]))
776
+ )
777
+
778
+ for instance in instances:
779
+ instance.aliases.append("new_alias")
780
+
781
+ # Upsert with default options (merge, keep unset fields)
782
+ engine.upsert(instances)
783
+
784
+ # Upsert with replace=True (replace entire instance)
785
+ engine.upsert(instances, replace=True)
786
+
787
+ # Upsert with remove_unset=True (remove fields not set in model)
788
+ engine.upsert(instances, remove_unset=True)
789
+ ```
790
+
791
+ ### Create New Instances
792
+
793
+ ```python
794
+ from industrial_model import WritableViewInstance, InstanceId, ViewInstanceConfig
795
+
796
+ class CogniteAsset(WritableViewInstance):
797
+ view_config = ViewInstanceConfig(view_external_id="CogniteAsset")
798
+ name: str
799
+ aliases: list[str]
800
+
801
+ def edge_id_factory(self, target_node: InstanceId, edge_type: InstanceId) -> InstanceId:
802
+ return InstanceId(
803
+ external_id=f"{self.external_id}-{target_node.external_id}-{edge_type.external_id}",
804
+ space=self.space,
805
+ )
806
+
807
+ # Create new instances
808
+ new_asset = CogniteAsset(
809
+ external_id="NEW-ASSET-001",
810
+ space="cdf_cdm",
811
+ name="New Asset",
812
+ aliases=["alias1", "alias2"],
813
+ )
814
+
815
+ engine.upsert([new_asset])
816
+ ```
817
+
818
+ ### Delete Instances
819
+
820
+ ```python
821
+ from industrial_model import search, col
822
+
823
+ # Find instances to delete
824
+ instances_to_delete = engine.search(
825
+ search(CogniteAsset)
826
+ .where(col(CogniteAsset.aliases).contains_any_(["old_alias"]))
827
+ .query_by("obsolete", [CogniteAsset.name])
828
+ )
829
+
830
+ # Delete them
831
+ engine.delete(instances_to_delete)
832
+ ```
833
+
834
+ ---
835
+
836
+ ## 🚀 Advanced Features
837
+
838
+ Utilities for ID generation, `InstanceId` handling, and working with `PaginatedResult`.
839
+
840
+ ### Generate Model IDs
841
+
842
+ Generate IDs from model fields:
843
+
844
+ ```python
845
+ from industrial_model import ViewInstance, ViewInstanceConfig
846
+
847
+ class CogniteAsset(ViewInstance):
848
+ view_config = ViewInstanceConfig(
849
+ view_external_id="CogniteAsset",
850
+ view_code="ASSET",
851
+ )
852
+ name: str
853
+ space: str
854
+
855
+ asset = CogniteAsset(
856
+ external_id="",
857
+ space="cdf_cdm",
858
+ name="Pump-001",
859
+ space="Industrial-Data",
860
+ )
861
+
862
+ # Generate ID from name
863
+ id_from_name = asset.generate_model_id(["name"])
864
+ # Result: "ASSET-Pump-001"
865
+
866
+ # Generate ID from multiple fields
867
+ id_from_fields = asset.generate_model_id(["space", "name"])
868
+ # Result: "ASSET-Industrial-Data-Pump-001"
869
+
870
+ # Without view_code prefix
871
+ id_no_prefix = asset.generate_model_id(["name"], view_code_as_prefix=False)
872
+ # Result: "Pump-001"
873
+
874
+ # Custom separator
875
+ id_custom = asset.generate_model_id(["space", "name"], separator="_")
876
+ # Result: "ASSET-Industrial-Data_Pump-001"
877
+ ```
878
+
879
+ ### InstanceId Operations
880
+
881
+ ```python
882
+ from industrial_model import InstanceId
883
+
884
+ # Create InstanceId
885
+ asset_id = InstanceId(external_id="ASSET-001", space="cdf_cdm")
886
+
887
+ # Convert to tuple
888
+ space, external_id = asset_id.as_tuple()
889
+
890
+ # Use in comparisons
891
+ other_id = InstanceId(external_id="ASSET-001", space="cdf_cdm")
892
+ assert asset_id == other_id
893
+
894
+ # Use as dictionary key (InstanceId is hashable)
895
+ id_map = {asset_id: "some_value"}
896
+ ```
897
+
898
+ ### PaginatedResult Utilities
899
+
900
+ ```python
901
+ from industrial_model import select
902
+
903
+ statement = select(CogniteAsset).limit(100)
904
+ result = engine.query(statement)
905
+
906
+ # Get first item or None
907
+ first_asset = result.first_or_default()
908
+
909
+ # Check if there are more pages
910
+ if result.has_next_page:
911
+ next_cursor = result.next_cursor
912
+ # Use cursor for next page
913
+ ```
914
+
915
+ ---
916
+
917
+ ## ⚡ Async Operations
918
+
919
+ Use `AsyncEngine` for async code. Every sync method has an `_async` counterpart (e.g. `query_async`, `upsert_async`).
920
+
921
+ ### AsyncEngine Setup
922
+
923
+ ```python
924
+ from industrial_model import AsyncEngine
925
+ from pathlib import Path
926
+
927
+ async_engine = AsyncEngine.from_config_file(Path("cognite-sdk-config.yaml"))
928
+ ```
929
+
930
+ ### Async Query Operations
931
+
932
+ ```python
933
+ from industrial_model import select, col
934
+
935
+ # Async query
936
+ statement = select(CogniteAsset).where(col(CogniteAsset.name).prefix("Pump-"))
937
+ result = await async_engine.query_async(statement)
938
+
939
+ # Async query all pages
940
+ all_results = await async_engine.query_all_pages_async(statement)
941
+
942
+ # Async search
943
+ search_statement = search(CogniteAsset).query_by("pump")
944
+ results = await async_engine.search_async(search_statement)
945
+
946
+ # Async aggregate
947
+ aggregate_statement = aggregate(CogniteAssetByName, "count")
948
+ results = await async_engine.aggregate_async(aggregate_statement)
949
+ ```
950
+
951
+ ### Async Write Operations
952
+
953
+ ```python
954
+ # Async upsert
955
+ instances = [new_asset1, new_asset2]
956
+ await async_engine.upsert_async(instances, replace=False, remove_unset=False)
957
+
958
+ # Async delete
959
+ await async_engine.delete_async(instances_to_delete)
960
+ ```
961
+
962
+ ### Complete Async Example
963
+
964
+ ```python
965
+ import asyncio
966
+ from industrial_model import AsyncEngine, select, col
967
+ from pathlib import Path
968
+
969
+ async def main():
970
+ engine = AsyncEngine.from_config_file(Path("cognite-sdk-config.yaml"))
971
+
972
+ # Run multiple queries concurrently
973
+ statement1 = select(CogniteAsset).where(col(CogniteAsset.name).prefix("Pump-"))
974
+ statement2 = select(CogniteAsset).where(col(CogniteAsset.name).prefix("Valve-"))
975
+
976
+ results1, results2 = await asyncio.gather(
977
+ engine.query_all_pages_async(statement1),
978
+ engine.query_all_pages_async(statement2),
979
+ )
980
+
981
+ print(f"Found {len(results1)} pumps and {len(results2)} valves")
982
+
983
+ asyncio.run(main())
984
+ ```
985
+
986
+ ---
987
+
988
+ ## 📝 Complete Example
989
+
990
+ Putting it together: query with filters, search, aggregate, upsert, and delete in one script.
991
+
992
+ ```python
993
+ from industrial_model import (
994
+ Engine,
995
+ ViewInstance,
996
+ WritableViewInstance,
997
+ ViewInstanceConfig,
998
+ InstanceId,
999
+ select,
1000
+ search,
1001
+ aggregate,
1002
+ AggregatedViewInstance,
1003
+ col,
1004
+ and_,
1005
+ or_,
1006
+ )
1007
+ from pathlib import Path
1008
+
1009
+ # Define models
1010
+ class CogniteAsset(WritableViewInstance):
1011
+ view_config = ViewInstanceConfig(
1012
+ view_external_id="CogniteAsset",
1013
+ instance_spaces_prefix="Industrial-",
1014
+ )
1015
+ name: str
1016
+ description: str | None = None
1017
+ aliases: list[str] = []
1018
+ parent: CogniteAsset | None = None
1019
+
1020
+ def edge_id_factory(self, target_node: InstanceId, edge_type: InstanceId) -> InstanceId:
1021
+ return InstanceId(
1022
+ external_id=f"{self.external_id}-{target_node.external_id}-{edge_type.external_id}",
1023
+ space=self.space,
1024
+ )
1025
+
1026
+ class AssetCountByParent(AggregatedViewInstance):
1027
+ view_config = ViewInstanceConfig(view_external_id="CogniteAsset")
1028
+ parent: InstanceId | None = None
1029
+
1030
+ # Setup engine
1031
+ engine = Engine.from_config_file(Path("cognite-sdk-config.yaml"))
1032
+
1033
+ # 1. Query with complex filters
1034
+ statement = (
1035
+ select(CogniteAsset)
1036
+ .where(
1037
+ and_(
1038
+ col(CogniteAsset.aliases).contains_any_(["pump", "equipment"]),
1039
+ col(CogniteAsset.description).exists_(),
1040
+ or_(
1041
+ col(CogniteAsset.parent).nested_(col(CogniteAsset.name) == "Root Asset"),
1042
+ col(CogniteAsset.name).prefix("Pump-"),
1043
+ ),
1044
+ )
1045
+ )
1046
+ .asc(CogniteAsset.name)
1047
+ .limit(100)
1048
+ )
1049
+
1050
+ results = engine.query(statement)
1051
+ print(f"Found {len(results.data)} assets")
1052
+
1053
+ # 2. Search with filters
1054
+ search_results = engine.search(
1055
+ search(CogniteAsset)
1056
+ .where(col(CogniteAsset.aliases).contains_any_(["pump"]))
1057
+ .query_by("industrial equipment", [CogniteAsset.name, CogniteAsset.description])
1058
+ )
1059
+
1060
+ # 3. Aggregate
1061
+ aggregate_results = engine.aggregate(
1062
+ aggregate(AssetCountByParent, "count")
1063
+ .where(col(CogniteAsset.description).exists_())
1064
+ .group_by(col(AssetCountByParent.parent))
1065
+ )
1066
+
1067
+ for result in aggregate_results:
1068
+ print(f"Parent: {result.parent}, Count: {result.value}")
1069
+
1070
+ # 4. Update instances
1071
+ assets = engine.query_all_pages(
1072
+ select(CogniteAsset).where(col(CogniteAsset.name).prefix("Pump-"))
1073
+ )
1074
+
1075
+ for asset in assets:
1076
+ if "legacy" not in asset.aliases:
1077
+ asset.aliases.append("legacy")
1078
+
1079
+ engine.upsert(assets, replace=False)
1080
+
1081
+ # 5. Delete obsolete assets
1082
+ obsolete = engine.search(
1083
+ search(CogniteAsset)
1084
+ .query_by("obsolete", [CogniteAsset.name])
1085
+ )
1086
+ engine.delete(obsolete)
1087
+ ```
1088
+
1089
+ ---
1090
+
1091
+ ## 🎯 Best Practices
1092
+
1093
+ 1. **Models** — Declare only the fields you use; smaller models stay clearer and faster
1094
+ 2. **View Configuration**: Use `instance_spaces` or `instance_spaces_prefix` to optimize queries
1095
+ 3. **Pagination**: Use `query_all_pages()` for small datasets, `query()` with cursors for large datasets
1096
+ 4. **Validation**: Use `ignoreOnError` mode when dealing with potentially inconsistent data
1097
+ 5. **Edge Relationships**: Always implement `edge_id_factory` for writable models with relationships
1098
+ 6. **Async Operations**: Use async methods when making multiple concurrent queries
1099
+ 7. **Filtering**: Use specific filters to reduce query size and improve performance
1100
+
1101
+ ---
1102
+
1103
+ ## 📚 Additional Resources
1104
+
1105
+ - [Cognite Data Fusion Documentation](https://docs.cognite.com/)
1106
+ - [Pydantic Documentation](https://docs.pydantic.dev/)
1107
+
1108
+ ---
1109
+
1110
+ ## 🤝 Contributing
1111
+
1112
+ Contributions are welcome! Please feel free to submit a Pull Request.
1113
+
1114
+ ---
1115
+
1116
+ ## 📄 License
1117
+
1118
+ See LICENSE file for details.