Skip to content

Commit 2480c77

Browse files
authored
Merge pull request #67 from dataiku/chore/refacto-parameters
Chore/refacto parameters
2 parents 17ed4a8 + cbd98da commit 2480c77

File tree

16 files changed

+231
-168
lines changed

16 files changed

+231
-168
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## [Version 1.3.0](https://github.com/dataiku/dss-plugin-pi-server/releases/tag/v1.3.0) - Feature and bug release - 2025-05-23
4+
5+
- Accept timezone in datetime inputs
6+
- Add a "Step" column ([Documentation](https://docs.aveva.com/bundle/pi-server-s-da-admin/page/1020817.html))
7+
- Add linear interpolation to Transpose & Sync recipe
8+
- Allows maxCount parameter tuning to allow faster data transfer (not available for Event frames related components - for these batch mode may be useful to speed up data transfer)
9+
- Add ElementName column to the Attribute Search connector
10+
311
## [Version 1.2.4](https://github.com/dataiku/dss-plugin-pi-server/releases/tag/v1.2.4) - Feature and bug release - 2025-02-18
412

513
- Add boundary type selector to recorded data type

custom-recipes/pi-system-retrieve-event-frames/recipe.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,10 +261,10 @@
261261
{
262262
"name": "max_count",
263263
"label": "Max count",
264-
"visibilityCondition": "['RecordedData'].includes(model.data_type)",
264+
"visibilityCondition": "false && model.show_advanced_parameters==true && ['InterpolatedData','PlotData','RecordedData'].includes(model.data_type)",
265265
"description": "",
266266
"type": "INT",
267-
"defaultValue": 1000
267+
"defaultValue": 10000
268268
}
269269
],
270270
"resourceKeys": []

custom-recipes/pi-system-retrieve-event-frames/recipe.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@
118118
batch_buffer_size += 1
119119
if (batch_buffer_size >= batch_size) or (absolute_index == nb_rows_to_process):
120120
rows = client.get_rows_from_webids(
121-
buffer, data_type, max_count,
121+
buffer, data_type, max_count=max_count,
122122
search_full_hierarchy=search_full_hierarchy,
123123
can_raise=False,
124124
batch_size=batch_size,

custom-recipes/pi-system-retrieve-list/recipe.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,10 +274,10 @@
274274
{
275275
"name": "max_count",
276276
"label": "Max count",
277-
"visibilityCondition": "false && ['RecordedData'].includes(model.data_type)",
278-
"description": "",
277+
"visibilityCondition": "model.show_advanced_parameters==true && ['InterpolatedData','PlotData','RecordedData'].includes(model.data_type)",
278+
"description": "Larger number speeds data transfer but loads the PI server",
279279
"type": "INT",
280-
"defaultValue": 1000
280+
"defaultValue": 10000
281281
}
282282
],
283283
"resourceKeys": []

custom-recipes/pi-system-retrieve-list/recipe.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@
1818
logger.info("PIWebAPI Assets values downloader recipe v{}".format(
1919
OSIsoftConstants.PLUGIN_VERSION
2020
))
21+
22+
23+
def get_step_value(item):
24+
if item and "Step" in item:
25+
if item.get("Step") is True:
26+
return "True"
27+
else:
28+
return "False"
29+
return None
30+
31+
2132
input_dataset = get_input_names_for_role('input_dataset')
2233
output_names_stats = get_output_names_for_role('api_output')
2334
config = get_recipe_config()
@@ -99,6 +110,7 @@
99110
if client.is_resource_path(object_id):
100111
object_id = normalize_af_path(object_id)
101112
item = client.get_item_from_path(object_id)
113+
step_value = get_step_value(item)
102114
if item:
103115
rows = client.recursive_get_rows_from_item(
104116
item,
@@ -136,12 +148,12 @@
136148
row[path_column] = object_id
137149
if isinstance(row, list):
138150
for line in row:
139-
base = get_base_for_data_type(data_type, object_id)
151+
base = get_base_for_data_type(data_type, object_id, Step=step_value)
140152
base.update(line)
141153
extention = client.unnest_row(base)
142154
results.extend(extention)
143155
else:
144-
base = get_base_for_data_type(data_type, object_id)
156+
base = get_base_for_data_type(data_type, object_id, Step=step_value)
145157
if duplicate_initial_row:
146158
base.update(duplicate_initial_row)
147159
base.update(row)

custom-recipes/pi-system-transpose/recipe.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,34 @@
8282
"type": "STRING",
8383
"mandatory": true
8484
},
85+
{
86+
"name": "type_of_interpolation",
87+
"label": "Type of interpolation",
88+
"description": "",
89+
"type": "SELECT",
90+
"selectChoices": [
91+
{"value": "last_value", "label": "Last value received"},
92+
{"value": "interpolation", "label": "Interpolation"},
93+
{"value": "auto", "label": "Mixed (based on step)"}
94+
],
95+
"defaultValue": "last_value"
96+
},
97+
{
98+
"name": "step_column_name",
99+
"label": "Step column",
100+
"description": "Column containing the Step information",
101+
"type": "COLUMN",
102+
"allowedColumnTypes": [
103+
"tinyint",
104+
"smallint",
105+
"int",
106+
"bigint",
107+
"string",
108+
"boolean"
109+
],
110+
"columnRole": "input_dataset",
111+
"visibilityCondition": "model.type_of_interpolation=='auto'"
112+
},
85113
{
86114
"name": "show_advanced_parameters",
87115
"label": "Show advanced parameters",

custom-recipes/pi-system-transpose/recipe.py

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import dateutil.parser
1010
from column_name import normalise_name
1111
from osisoft_plugin_common import reorder_dataframe
12+
from datetime import datetime
1213

1314

1415
logger = SafeLogger("pi-system plugin", forbiden_keys=["token", "password"])
@@ -36,18 +37,27 @@ def parse_timestamp_and_value(line):
3637
return date, value
3738

3839

39-
def get_datetime_from_string(datetime):
40+
def get_epoch_from_string(datetime_string):
4041
try:
41-
_ = dateutil.parser.isoparse(datetime)
42-
return datetime
42+
utc_time = datetime.strptime(datetime_string, "%Y-%m-%dT%H:%M:%SZ")
43+
epoch_time = (utc_time - datetime(1970, 1, 1)).total_seconds()
44+
except Exception:
45+
return None
46+
return epoch_time
47+
48+
49+
def get_datetime_from_string(datetime_string):
50+
try:
51+
_ = dateutil.parser.isoparse(datetime_string)
52+
return datetime_string
4353
except Exception:
4454
pass
4555
return None
4656

4757

48-
def get_datetime_from_pandas(datetime):
58+
def get_datetime_from_pandas(datetime_string):
4959
try:
50-
time_stamp = datetime.strftime('%Y-%m-%dT%H:%M:%SZ')
60+
time_stamp = datetime_string.strftime('%Y-%m-%dT%H:%M:%SZ')
5161
return time_stamp
5262
except Exception:
5363
pass
@@ -63,7 +73,7 @@ def get_datetime_from_row(row, datetime_column):
6373
return formated_datetime
6474

6575

66-
def get_latest_values_at_timestamp(file_handles, seek_timestamp):
76+
def get_values_at_timestamp(file_handles, seek_timestamp, step_attributes):
6777
attribute_index = 0
6878
values = {}
6979
for attribute_path in file_handles:
@@ -85,19 +95,42 @@ def get_latest_values_at_timestamp(file_handles, seek_timestamp):
8595
next_timestamps_cache[attribute_index] = attribute_timestamp
8696
next_values_cache[attribute_index] = attribute_value
8797
next_cached_timestamp = next_timestamps_cache[attribute_index]
98+
if step_attributes.get(attribute_path) is True:
99+
calculated_value = interpolate(
100+
current_timestamps_cache[attribute_index],
101+
current_values_cache[attribute_index],
102+
next_timestamps_cache[attribute_index],
103+
next_values_cache[attribute_index],
104+
seek_timestamp
105+
)
106+
else:
107+
calculated_value = current_values_cache[attribute_index]
88108
if should_add_timestamps_columns:
89109
values.update({
90110
"{}{}".format(attribute_path, OSIsoftConstants.TIMESTAMP_COLUMN_SUFFIX): current_timestamps_cache[attribute_index],
91-
"{}{}".format(attribute_path, OSIsoftConstants.VALUE_COLUMN_SUFFIX): current_values_cache[attribute_index]
111+
"{}{}".format(attribute_path, OSIsoftConstants.VALUE_COLUMN_SUFFIX): calculated_value
92112
})
93113
else:
94114
values.update({
95-
attribute_path: current_values_cache[attribute_index]
115+
attribute_path: calculated_value
96116
})
97117
attribute_index = attribute_index + 1
98118
return values
99119

100120

121+
def interpolate(previous_timestamp, previous_value, next_timestamp, next_value, time_now):
122+
previous_timestamp = get_epoch_from_string(previous_timestamp)
123+
next_timestamp = get_epoch_from_string(next_timestamp)
124+
time_now = get_epoch_from_string(time_now)
125+
if previous_timestamp is None or next_timestamp is None or time_now is None:
126+
return None
127+
if previous_timestamp == next_timestamp or time_now == previous_timestamp:
128+
return previous_value
129+
rate_of_change = (float(next_value) - float(previous_value)) / (float(next_timestamp) - float(previous_timestamp))
130+
value_now = float(previous_value) + rate_of_change * (float(time_now) - float(previous_timestamp))
131+
return value_now
132+
133+
101134
def clean_cache(paths_to_file_handles):
102135
logger.info("Polling done, cleaning the cache files")
103136
# Close and delete all cache files
@@ -162,12 +195,17 @@ def get_column_name_specifications():
162195
previous_server_url = ""
163196
paths_to_file_handles = {}
164197
file_counter = 0
198+
step_attributes = {}
199+
200+
type_of_interpolation = config.get("type_of_interpolation", "last_value")
201+
if type_of_interpolation == "auto":
202+
step_column_name = config.get("step_column_name", "Step")
165203

166204
# Cache each attribute
167205
logger.info("Caching all attributes in {}".format(temp_location.name))
168206
for index, input_parameters_row in input_parameters_dataframe.iterrows():
169-
datetime = get_datetime_from_row(input_parameters_row, datetime_column)
170-
if not datetime:
207+
row_datetime = get_datetime_from_row(input_parameters_row, datetime_column)
208+
if not row_datetime:
171209
continue
172210
attribute_path = input_parameters_row.get(input_paths_column)
173211
if should_make_column_names_db_compatible:
@@ -181,7 +219,14 @@ def get_column_name_specifications():
181219
if attribute_path == reference_attribute_path:
182220
time_reference_file = file_counter
183221
file_counter = file_counter + 1
184-
paths_to_file_handles[attribute_path].writelines("{}|{}\n".format(datetime, value))
222+
paths_to_file_handles[attribute_path].writelines("{}|{}\n".format(row_datetime, value))
223+
224+
if type_of_interpolation == "auto":
225+
is_step_attribute = input_parameters_row.get(step_column_name)
226+
if is_step_attribute == "True" or is_step_attribute is True:
227+
step_attributes[attribute_path] = True
228+
elif type_of_interpolation == "interpolation":
229+
step_attributes[attribute_path] = True
185230

186231
logger.info("Cached all {} attributes".format(file_counter))
187232

@@ -210,15 +255,15 @@ def get_column_name_specifications():
210255
next_timestamps_cache.pop(0)
211256
next_values_cache.pop(0)
212257

258+
logger.info("Polling all attributes into final dataset")
213259
# For each timestamp of synchronizer attribute, read the most up to date value of all other attributes
214260
# Write all that, one column per attribute
215261
first_dataframe = True
216-
logger.info("Polling all attributes into final dataset")
217262
with output_dataset.get_writer() as writer:
218263
for line in reference_values_file:
219264
unnested_items_rows = []
220265
timestamp, value = parse_timestamp_and_value(line)
221-
output_columns_dictionary = get_latest_values_at_timestamp(paths_to_file_handles, timestamp)
266+
output_columns_dictionary = get_values_at_timestamp(paths_to_file_handles, timestamp, step_attributes)
222267
output_columns_dictionary.update({
223268
OSIsoftConstants.TIMESTAMP_COLUMN_NAME: timestamp,
224269
reference_attribute_path: value

plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"id": "pi-system",
3-
"version": "1.2.4",
3+
"version": "1.3.0",
44
"meta": {
55
"label": "PI System",
66
"description": "Retrieve data from your OSIsoft PI System servers",

python-connectors/pi-system_attribute-search/connector.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -394,10 +394,10 @@
394394
{
395395
"name": "max_count",
396396
"label": "Max count",
397-
"visibilityCondition": "false && model.must_retrieve_metrics && ['InterpolatedData', 'RecordedData'].includes(model.data_type)",
397+
"visibilityCondition": "model.show_advanced_parameters==true && model.must_retrieve_metrics && ['PlotData', 'InterpolatedData', 'RecordedData'].includes(model.data_type)",
398398
"description": "",
399399
"type": "INT",
400-
"defaultValue": 1000
400+
"defaultValue": 10000
401401
}
402402
]
403403
}

python-connectors/pi-system_attribute-search/connector.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,10 @@ def generate_rows(self, dataset_schema=None, dataset_partitioning=None,
121121
):
122122
if limit.is_reached():
123123
return
124-
output_row = format_output(row, attribute, is_enumeration_value=is_enumeration_value)
124+
output_row = format_output(
125+
row, attribute,
126+
is_enumeration_value=is_enumeration_value
127+
)
125128
yield output_row
126129
else:
127130
for row in self.client.search_attributes(

0 commit comments

Comments
 (0)