Writing Instances: Upsert¶
We assume that you have generated a SDK for the WindTurbine
model and have a client ready to go.
The SDK generated by SDK
supports nested upsert
from wind_turbine import WindTurbineClient
pygen = WindTurbineClient.from_toml("config.toml")
Constructing new Turbine¶
When constructing a new WindTurbine we need to use the generated data classes from pygen
.
We can import this as follows:
from wind_turbine import data_classes as data_cls
The location of the data classes is determined by the parameter top_level_package
which is set when you generate the SDK,
either using the generate_sdk_notebook
(simplified wrapper around) or generate_sdk
. If you don't set it, it will be
default set to the external_id of the data model converted to snake_case. For this example, the external_id=WindTurbine
thus the top_level_package = wind_turbine
.
Lets construct a new windmill with TimeSeries. Note the example below is not complete (some TimeSeries and components are missing), but is kept short to make it easier to grasp
new_turbine = data_cls.WindTurbineWrite(
external_id="windmill:demo",
capacity=10.0,
windfarm="Fornebu",
name="Windmill ATH",
rotor=data_cls.RotorWrite(
external_id="windmill:demo:rotor",
),
nacelle=data_cls.NacelleWrite(
external_id="windmill:demo:nacelle",
),
blades=[
data_cls.BladeWrite(
external_id="windmill:demo:blade1",
is_damaged=False,
name="Blade 1",
),
data_cls.BladeWrite(
external_id="windmill:demo:blade2",
is_damaged=False,
name="Blade 2",
),
data_cls.BladeWrite(
external_id="windmill:demo:blade3",
is_damaged=True,
name="Blade 3",
),
],
)
When writing nested data we can specify edges either with an external id for the end node, or another data class.
The advangage of using a nested data class is that we can express edges without being explicit. In the example above, we are able to express that the blades Blade 1-3
are connected to the windmill windmill ATH
and that the windmill ATH
is also linked to a nacelle and rotor.
Inspecting Resources to create¶
We can inspect the nodes, edges and other resources that will be created by using the .to_instances_write
on the new wind turbine object.
resources = new_turbine.to_instances_write()
len(resources.nodes), len(resources.edges), len(resources.time_series)
(6, 0, 0)
resources.nodes
space | external_id | instance_type | sources | |
---|---|---|---|---|
0 | sp_wind | windmill:demo | node | [{'properties': {'blades': [{'space': 'sp_wind... |
1 | sp_wind | windmill:demo:blade1 | node | [{'properties': {'is_damaged': False, 'name': ... |
2 | sp_wind | windmill:demo:blade2 | node | [{'properties': {'is_damaged': False, 'name': ... |
3 | sp_wind | windmill:demo:blade3 | node | [{'properties': {'is_damaged': True, 'name': '... |
4 | sp_wind | windmill:demo:nacelle | node | NaN |
5 | sp_wind | windmill:demo:rotor | node | NaN |
resources.edges
resources.time_series
Creating new WindTurbine¶
Optinal Reading: Why pygen.upsert
and not pygen.wind_turbine.upsert
?
In contrast from other methods, the .upsert
method is on the pygen
instead of the individual API class. So instead of pygen.wind_turbine.upsert
, we use pygen.upsert
.
The reason for this is that the new_turbine
we created above is enhanced by pygen
with all the information needed to write it correctly to our data model. This means that all .upsert
methods are the same, this is in contrast to methods such as .list
and .retrieve
which are specialized for each data type.
Furthermore, the reason for not duplicating the .upsert
methods on each API class (pygen.wind_turbine.upsert
, pygen.blade.upsert
, and so on) is that encourages an anti-pattern (bad practice), in which nodes and edges are created in small batches. It is much more efficient to create all nodes and edges in as few batches as possible.
created = pygen.upsert(new_turbine)
Note that the call above created 6 nodes. If you had linked metmasts, it would have created edges as well.
created.nodes
space | instance_type | external_id | version | was_modified | last_updated_time | created_time | |
---|---|---|---|---|---|---|---|
0 | sp_wind | node | windmill:demo | 1 | True | 2024-12-29 11:23:45.015 | 2024-12-29 11:23:45.015 |
1 | sp_wind | node | windmill:demo:blade1 | 1 | True | 2024-12-29 11:23:45.015 | 2024-12-29 11:23:45.015 |
2 | sp_wind | node | windmill:demo:blade2 | 1 | True | 2024-12-29 11:23:45.015 | 2024-12-29 11:23:45.015 |
3 | sp_wind | node | windmill:demo:blade3 | 1 | True | 2024-12-29 11:23:45.015 | 2024-12-29 11:23:45.015 |
4 | sp_wind | node | windmill:demo:nacelle | 1 | False | 2024-11-16 17:30:21.812 | 2024-11-16 17:30:21.812 |
5 | sp_wind | node | windmill:demo:rotor | 1 | False | 2024-11-16 17:30:21.812 | 2024-11-16 17:30:21.812 |
created.edges
created.time_series
We can inspect the newly created windmill by calling retrieve with the external id
pygen.wind_turbine.retrieve(new_turbine.external_id)
value | |
---|---|
space | sp_wind |
external_id | windmill:demo |
data_record | {'version': 1, 'last_updated_time': 2024-12-29... |
node_type | None |
capacity | 10.0 |
description | None |
name | Windmill ATH |
blades | [windmill:demo:blade1, windmill:demo:blade2, w... |
datasheets | [] |
metmast | None |
nacelle | windmill:demo:nacelle |
power_curve | None |
rotor | windmill:demo:rotor |
windfarm | Fornebu |
Upsert Parameters replace
, None
values and allow_version_increase
¶
The upsert method have several parameters that control how the upsert call should be done. In this section, we will go through each of these flags.
Parameter: replace
¶
The replace
flag decide what to do if the item we are upserting already exists. If replace
is set to True
all properties of the existing item will be replaced by the properties set in the upsert call,
and the properties not included will be to null. If replace
is set to False
, then only the properties included in the upsert call will be updated.
Let's demonstrate this by creating a new blade and update it. We start by creating a new blade and call upsert on it.
A blade has two properties: name
and is_damaged
. In addition, it has edges to the sensor_positions
connected to the blade. In this example, we will focus on the two properties, which are both nullable, meaning that they are optional.
from wind_turbine import data_classes as data_cls
new_blade = data_cls.BladeWrite(
external_id="windmill:demo:blade4",
name="Demo Blade",
)
created_blade = pygen.upsert(new_blade)
created_blade.nodes
space | instance_type | external_id | version | was_modified | last_updated_time | created_time | |
---|---|---|---|---|---|---|---|
0 | sp_wind | node | windmill:demo:blade4 | 1 | True | 2024-12-29 11:24:55.634 | 2024-12-29 11:24:55.634 |
retrieved_blade = pygen.blade.retrieve(new_blade.external_id)
retrieved_blade
value | |
---|---|
space | sp_wind |
external_id | windmill:demo:blade4 |
data_record | {'version': 1, 'last_updated_time': 2024-12-29... |
node_type | None |
is_damaged | None |
name | Demo Blade |
sensor_positions | None |
We can see that the is_damaged
property is not set, while we have the name
property set. We will now update the blade with the is_damaged
property set to True
.
updated_blade = data_cls.BladeWrite(
external_id="windmill:demo:blade4",
is_damaged=True,
)
update = pygen.upsert(updated_blade, replace=False)
update.nodes
space | instance_type | external_id | version | was_modified | last_updated_time | created_time | |
---|---|---|---|---|---|---|---|
0 | sp_wind | node | windmill:demo:blade4 | 2 | True | 2024-12-29 11:25:36.498 | 2024-12-29 11:24:55.634 |
retrieved_blade = pygen.blade.retrieve(new_blade.external_id)
retrieved_blade
value | |
---|---|
space | sp_wind |
external_id | windmill:demo:blade4 |
data_record | {'version': 2, 'last_updated_time': 2024-12-29... |
node_type | None |
is_damaged | True |
name | Demo Blade |
sensor_positions | None |
We see that the blade property is_damaged
is now set to True
, while the name
property is unchanged. This is because we set replace
to False
. If we set replace
to True
, then the name
property would be set to null
. Let's create a new update were we set the is_damaged
property to False
and use replace
set to True
.
blade_update2 = data_cls.BladeWrite(
external_id="windmill:demo:blade4",
is_damaged=False,
)
update2 = pygen.upsert(blade_update2, replace=True)
update2.nodes
space | instance_type | external_id | version | was_modified | last_updated_time | created_time | |
---|---|---|---|---|---|---|---|
0 | sp_wind | node | windmill:demo:blade4 | 3 | True | 2024-12-29 11:27:00.629 | 2024-12-29 11:24:55.634 |
retrieved_blade = pygen.blade.retrieve(new_blade.external_id)
retrieved_blade
value | |
---|---|
space | sp_wind |
external_id | windmill:demo:blade4 |
data_record | {'version': 3, 'last_updated_time': 2024-12-29... |
node_type | None |
is_damaged | False |
name | None |
sensor_positions | None |
Notice that the name
property is now set to null
as we set replace
to True
.
# Cleanup
pygen.delete(new_blade.external_id)
InstancesDeleteResult(nodes=[NodeId(space='sp_wind', external_id='windmill:demo:blade4')], edges=[])
None
values¶
pygen
automatically detects the nullable values that are set to None
and sends them to CDF.
Let's demonstrate this by creating a new blade with the is_damaged
property set to True
, and then update it to be not set.
new_blade = data_cls.BladeWrite(external_id="windmill:demo:write_none", name="Demo Blade", is_damaged=True)
pygen.upsert(new_blade).nodes
space | instance_type | external_id | version | was_modified | last_updated_time | created_time | |
---|---|---|---|---|---|---|---|
0 | sp_wind | node | windmill:demo:write_none | 1 | True | 2024-12-29 11:32:34.539 | 2024-12-29 11:32:34.539 |
retrieved_blade = pygen.blade.retrieve(new_blade.external_id)
retrieved_blade
value | |
---|---|
space | sp_wind |
external_id | windmill:demo:write_none |
data_record | {'version': 1, 'last_updated_time': 2024-12-29... |
node_type | None |
is_damaged | True |
name | Demo Blade |
sensor_positions | None |
updated_blade = data_cls.BladeWrite(
external_id="windmill:demo:write_none",
is_damaged=None,
)
pygen.upsert(updated_blade).nodes
space | instance_type | external_id | version | was_modified | last_updated_time | created_time | |
---|---|---|---|---|---|---|---|
0 | sp_wind | node | windmill:demo:write_none | 2 | True | 2024-12-29 11:32:37.399 | 2024-12-29 11:32:34.539 |
retrieved_blade = pygen.blade.retrieve(new_blade.external_id)
retrieved_blade
value | |
---|---|
space | sp_wind |
external_id | windmill:demo:write_none |
data_record | {'version': 2, 'last_updated_time': 2024-12-29... |
node_type | None |
is_damaged | None |
name | Demo Blade |
sensor_positions | None |
Notice that the is_damaged
property is now set to None
.
# Cleanup
pygen.delete("windmill:demo:write_none")
InstancesDeleteResult(nodes=[NodeId(space='sp_wind', external_id='windmill:demo:write_none')], edges=[])
Parameter: allow_version_increase
¶
If you notice in the last examples, that when updating the blade, the version of the returning node is increasing. This is because each time we do a change to the blade node it is registered and the version is increased. You can control this behavior by setting the existing_version
property in data_record
of the blade node. Let's demonstrate with an example
from cognite.client.exceptions import CogniteAPIError
from wind_turbine import data_classes as data_cls
Introduction¶
new_blade = data_cls.BladeWrite(external_id="windmill:demo:allow_version_increase", name="Demo Blade", is_damaged=True)
pygen.upsert(new_blade).nodes
space | instance_type | external_id | version | was_modified | last_updated_time | created_time | |
---|---|---|---|---|---|---|---|
0 | sp_wind | node | windmill:demo:allow_version_increase | 1 | True | 2024-12-29 11:39:38.345 | 2024-12-29 11:39:38.345 |
We see that we have version 1 of the blade. If we want to avoid overwriting this blade by accident, we can set the existing_version
property to lower than the version we want to avoid overwriting in the data_record
of the blade node. This will be 0
in this case.
new_blade2 = data_cls.BladeWrite(
external_id="windmill:demo:allow_version_increase",
is_damaged=False,
data_record=data_cls.DataRecordWrite(existing_version=0),
)
try:
pygen.upsert(new_blade2)
except CogniteAPIError as e:
print(e)
A version conflict caused the ingest to fail. | code: 400 | X-Request-ID: e69cddfb-5fdd-9e7d-bce4-20cedee43c01 | cluster: greenfield The API Failed to process some items. Successful (2xx): [] Unknown (5xx): [] Failed (4xx): [sp_wind:windmill:demo:allow_version_increase, ...]
Allow Version Increase¶
This can cause problems when we want to migrate data from one project to another, or from one data model to another. It is a common pattern that we use the pygen
generated SDK to retrieve from one project and then use the .as_write
method to turn the retrieved read format of a node into the write format. We want to ensure we always will overwrite the nodes in the new project. Then, we can use the allow_version_increase
flag to ensure that we always overwrite the all the nodes and edges we are writing will have set existing_version
to None
which will ensure that we always overwrite the nodes and edges.
First, we retrieve the blade.
retrieved_blade = pygen.blade.retrieve(new_blade.external_id)
retrieved_blade
value | |
---|---|
space | sp_wind |
external_id | windmill:demo:allow_version_increase |
data_record | {'version': 1, 'last_updated_time': 2024-12-29... |
node_type | None |
is_damaged | True |
name | Demo Blade |
sensor_positions | None |
Before we write the blade back, somebody else updates it:
pygen.upsert(
data_cls.BladeWrite(external_id="windmill:demo:allow_version_increase", name="Updated", is_damaged=True)
).nodes
space | instance_type | external_id | version | was_modified | last_updated_time | created_time | |
---|---|---|---|---|---|---|---|
0 | sp_wind | node | windmill:demo:allow_version_increase | 2 | True | 2024-12-29 11:42:59.519 | 2024-12-29 11:39:38.345 |
Then, we try to write back the retrieved blade
writeable_blade = retrieved_blade.as_write()
writeable_blade
value | |
---|---|
space | sp_wind |
external_id | windmill:demo:allow_version_increase |
data_record | {'existing_version': 1} |
node_type | None |
is_damaged | True |
name | Demo Blade |
try:
pygen.upsert(writeable_blade)
except CogniteAPIError as e:
print(e)
A version conflict caused the ingest to fail. | code: 400 | X-Request-ID: 9de7d7cf-6b6e-9f2f-ba3f-6b61ec243363 | cluster: greenfield The API Failed to process some items. Successful (2xx): [] Unknown (5xx): [] Failed (4xx): [sp_wind:windmill:demo:allow_version_increase, ...]
We force the issue by setting allow_version_increase=True
pygen.upsert(writeable_blade, allow_version_increase=True).nodes
space | instance_type | external_id | version | was_modified | last_updated_time | created_time | |
---|---|---|---|---|---|---|---|
0 | sp_wind | node | windmill:demo:allow_version_increase | 3 | True | 2024-12-29 11:43:52.328 | 2024-12-29 11:39:38.345 |
We see that the allow_version_increase
flag ensures that we always overwrite the nodes and edges.
# Cleanup
pygen.delete("windmill:demo:allow_version_increase")
InstancesDeleteResult(nodes=[NodeId(space='sp_wind', external_id='windmill:demo:allow_version_increase')], edges=[])
Creating from JSON
Format¶
See the quick start guide data population for an example of creating instances from JSON
.
Deleting Instances¶
You can delete by passing and external ID or an Item or a sequence of external id or items to the delete method.
We can delete the newly created windmill
pygen.wind_turbine.list()
space | external_id | capacity | name | blades | datasheets | nacelle | rotor | windfarm | data_record | |
---|---|---|---|---|---|---|---|---|---|---|
0 | sp_wind | hornsea_1_mill_3 | 7.0 | hornsea_1_mill_3 | [hornsea_1_mill_3_blade_A, hornsea_1_mill_3_bl... | [windmill_schematics] | hornsea_1_mill_3_nacelle | hornsea_1_mill_3_rotor | Hornsea 1 | {'version': 8, 'last_updated_time': 2024-12-17... |
1 | sp_wind | hornsea_1_mill_2 | 7.0 | hornsea_1_mill_2 | [hornsea_1_mill_2_blade_B, hornsea_1_mill_2_bl... | [windmill_schematics] | hornsea_1_mill_2_nacelle | hornsea_1_mill_2_rotor | Hornsea 1 | {'version': 8, 'last_updated_time': 2024-12-17... |
2 | sp_wind | hornsea_1_mill_1 | 7.0 | hornsea_1_mill_1 | [hornsea_1_mill_1_blade_A, hornsea_1_mill_1_bl... | [windmill_schematics] | hornsea_1_mill_1_nacelle | hornsea_1_mill_1_rotor | Hornsea 1 | {'version': 8, 'last_updated_time': 2024-12-17... |
3 | sp_wind | hornsea_1_mill_4 | 7.0 | hornsea_1_mill_4 | [hornsea_1_mill_4_blade_C, hornsea_1_mill_4_bl... | [windmill_schematics] | hornsea_1_mill_4_nacelle | hornsea_1_mill_4_rotor | Hornsea 1 | {'version': 8, 'last_updated_time': 2024-12-17... |
4 | sp_wind | hornsea_1_mill_5 | 7.0 | hornsea_1_mill_5 | [hornsea_1_mill_5_blade_B, hornsea_1_mill_5_bl... | [windmill_schematics] | hornsea_1_mill_5_nacelle | hornsea_1_mill_5_rotor | Hornsea 1 | {'version': 8, 'last_updated_time': 2024-12-17... |
5 | sp_wind | windmill:demo | 10.0 | Windmill ATH | [windmill:demo:blade1, windmill:demo:blade2, w... | [] | windmill:demo:nacelle | windmill:demo:rotor | Fornebu | {'version': 1, 'last_updated_time': 2024-12-29... |
Same as .upsert
, the delete method is located on the pygen and not the API class.
deleted = pygen.delete(new_turbine)
len(deleted.nodes), len(deleted.edges)
(6, 0)
After the delete call the new windmill is gone
pygen.wind_turbine.list()
space | external_id | capacity | name | blades | datasheets | nacelle | rotor | windfarm | data_record | |
---|---|---|---|---|---|---|---|---|---|---|
0 | sp_wind | hornsea_1_mill_3 | 7.0 | hornsea_1_mill_3 | [hornsea_1_mill_3_blade_A, hornsea_1_mill_3_bl... | [windmill_schematics] | hornsea_1_mill_3_nacelle | hornsea_1_mill_3_rotor | Hornsea 1 | {'version': 8, 'last_updated_time': 2024-12-17... |
1 | sp_wind | hornsea_1_mill_2 | 7.0 | hornsea_1_mill_2 | [hornsea_1_mill_2_blade_B, hornsea_1_mill_2_bl... | [windmill_schematics] | hornsea_1_mill_2_nacelle | hornsea_1_mill_2_rotor | Hornsea 1 | {'version': 8, 'last_updated_time': 2024-12-17... |
2 | sp_wind | hornsea_1_mill_1 | 7.0 | hornsea_1_mill_1 | [hornsea_1_mill_1_blade_A, hornsea_1_mill_1_bl... | [windmill_schematics] | hornsea_1_mill_1_nacelle | hornsea_1_mill_1_rotor | Hornsea 1 | {'version': 8, 'last_updated_time': 2024-12-17... |
3 | sp_wind | hornsea_1_mill_4 | 7.0 | hornsea_1_mill_4 | [hornsea_1_mill_4_blade_C, hornsea_1_mill_4_bl... | [windmill_schematics] | hornsea_1_mill_4_nacelle | hornsea_1_mill_4_rotor | Hornsea 1 | {'version': 8, 'last_updated_time': 2024-12-17... |
4 | sp_wind | hornsea_1_mill_5 | 7.0 | hornsea_1_mill_5 | [hornsea_1_mill_5_blade_B, hornsea_1_mill_5_bl... | [windmill_schematics] | hornsea_1_mill_5_nacelle | hornsea_1_mill_5_rotor | Hornsea 1 | {'version': 8, 'last_updated_time': 2024-12-17... |
In addition, since we passed in the entire object and not just an external ID, all the edges and nodes connected to the windmill:demo
was also deleted.