diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2059534..e7705fb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+## [0.0.9] - 2026-09-10
+- singletsframe support
+
## [0.0.8] - 2026-07-07
- add new plotting-module to wheel
diff --git a/docs/genindex.html b/docs/genindex.html
index 9f53d9d..d69b792 100644
--- a/docs/genindex.html
+++ b/docs/genindex.html
@@ -64,19 +64,33 @@
G
get_series_display_name() (in module hdhelpers.metadata)
-
- |
+
diff --git a/docs/objects.inv b/docs/objects.inv
index 495e45d..4812edf 100644
Binary files a/docs/objects.inv and b/docs/objects.inv differ
diff --git a/docs/searchindex.js b/docs/searchindex.js
index a5da6b5..48248e2 100644
--- a/docs/searchindex.js
+++ b/docs/searchindex.js
@@ -1 +1 @@
-Search.setIndex({"alltitles":{"First steps":[[0,null]],"Functions":[[1,"functions"]],"Further Information":[[1,"further-information"]],"How to get metadata with hdhelpers?":[[0,"how-to-get-metadata-with-hdhelpers"]],"How to use hdhelpers for plotting? (tbd)":[[0,"how-to-use-hdhelpers-for-plotting-tbd"]],"Introduction":[[1,"introduction"]],"exceptions":[[1,"module-hdhelpers.exceptions"]],"hdhelpers":[[1,null]],"helpers":[[1,"module-hdhelpers.helpers"]],"metadata":[[1,"module-hdhelpers.metadata"]],"plotting":[[1,"module-hdhelpers.plotting"]]},"docnames":["first_steps","index"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["first_steps.rst","index.rst"],"indexentries":{"get_display_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_display_names",false]],"get_locale() (in module hdhelpers.helpers)":[[1,"hdhelpers.helpers.get_locale",false]],"get_measurements() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_measurements",false]],"get_metric_info() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_metric_info",false]],"get_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_names",false]],"get_queried_interval() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_queried_interval",false]],"get_series_display_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_display_name",false]],"get_series_info() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_info",false]],"get_series_measurement() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_measurement",false]],"get_series_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_name",false]],"get_series_short_display_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_short_display_name",false]],"get_series_unit() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_unit",false]],"get_short_display_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_short_display_names",false]],"get_units() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_units",false]],"hdhelpers.exceptions":[[1,"module-hdhelpers.exceptions",false]],"hdhelpers.helpers":[[1,"module-hdhelpers.helpers",false]],"hdhelpers.metadata":[[1,"module-hdhelpers.metadata",false]],"hdhelpers.plotting":[[1,"module-hdhelpers.plotting",false]],"helperexception":[[1,"hdhelpers.exceptions.HelperException",false]],"insufficientplottingdata":[[1,"hdhelpers.exceptions.InsufficientPlottingData",false]],"modify_timezone() (in module hdhelpers.helpers)":[[1,"hdhelpers.helpers.modify_timezone",false]],"module":[[1,"module-hdhelpers.exceptions",false],[1,"module-hdhelpers.helpers",false],[1,"module-hdhelpers.metadata",false],[1,"module-hdhelpers.plotting",false]],"resolve_color() (in module hdhelpers.plotting)":[[1,"hdhelpers.plotting.resolve_color",false]],"set_agnostic_theme() (in module hdhelpers.plotting)":[[1,"hdhelpers.plotting.set_agnostic_theme",false]],"set_dt_ticks() (in module hdhelpers.plotting)":[[1,"hdhelpers.plotting.set_dt_ticks",false]]},"objects":{"hdhelpers":[[1,0,0,"-","exceptions"],[1,0,0,"-","helpers"],[1,0,0,"-","metadata"],[1,0,0,"-","plotting"]],"hdhelpers.exceptions":[[1,1,1,"","HelperException"],[1,1,1,"","InsufficientPlottingData"]],"hdhelpers.helpers":[[1,2,1,"","get_locale"],[1,2,1,"","modify_timezone"]],"hdhelpers.metadata":[[1,2,1,"","get_display_names"],[1,2,1,"","get_measurements"],[1,2,1,"","get_metric_info"],[1,2,1,"","get_names"],[1,2,1,"","get_queried_interval"],[1,2,1,"","get_series_display_name"],[1,2,1,"","get_series_info"],[1,2,1,"","get_series_measurement"],[1,2,1,"","get_series_name"],[1,2,1,"","get_series_short_display_name"],[1,2,1,"","get_series_unit"],[1,2,1,"","get_short_display_names"],[1,2,1,"","get_units"]],"hdhelpers.plotting":[[1,2,1,"","resolve_color"],[1,2,1,"","set_agnostic_theme"],[1,2,1,"","set_dt_ticks"]]},"objnames":{"0":["py","module","Python module"],"1":["py","exception","Python exception"],"2":["py","function","Python function"]},"objtypes":{"0":"py:module","1":"py:exception","2":"py:function"},"terms":{"000z":0,"00z":1,"01t01":1,"01t08":0,"05t13":1,"06t13":1,"5s":[],"A":1,"All":[],"As":[],"DO":0,"For":[0,1],"How":1,"If":1,"In":[0,1],"It":1,"NOT":0,"Our":0,"That":[],"The":[],"Then":[],"This":1,"To":1,"We":0,"With":[],"__data__":0,"__hd_wrapped_data_object__":0,"__metadata__":0,"abov":0,"abstract":1,"access":1,"accord":0,"activ":[],"add_config_set":[],"adequ":1,"adit":1,"adjust":1,"agnost":1,"all_timezon":1,"allow":1,"also":[],"ani":1,"anoth":1,"appli":[],"applic":1,"arbitrari":1,"arg":1,"associ":1,"assum":1,"attr":1,"autoexpand":[],"avail":1,"awar":[],"axe":1,"axes_label_color":[],"axi":0,"b":[],"background":1,"background_color":[],"bar":[],"base":1,"becaus":[],"behaviour":1,"berlin":1,"bool":1,"boolean":1,"by_metr":1,"c":1,"call":[],"can":[0,1],"case":1,"certain":1,"chosen":[],"circl":[],"class":1,"cm":0,"code":[0,1],"collect":1,"color":[0,1],"color_str":1,"colorway":[],"column":1,"column_nam":1,"communic":[],"compli":1,"compon":[0,1],"concret":1,"configur":[],"contain":1,"context":1,"contrast":1,"control":1,"convent":1,"convert":1,"convert_index":1,"convey":[],"correct":[],"correspond":[0,1],"current":[],"custom":1,"cut":[],"danger":[],"dark":1,"data":1,"datafram":1,"dataset_metadata":1,"date":1,"datetim":1,"datetime_tick_format":[],"de":1,"de_d":1,"def":0,"default":1,"default_titl":0,"default_unit":[],"defaultdict":1,"defin":[0,1],"deprec":[],"design":[0,1],"detail":[],"determin":[],"dict":1,"dictionari":1,"dimens":1,"direct":0,"display":1,"display_nam":1,"display_name_of_metric1":1,"display_name_of_seri":1,"displaylogo":[],"displaymodebar":[],"doe":[],"e":1,"ea1255":1,"eas":1,"eb6962":1,"edg":[],"edit":0,"empti":1,"en":1,"enabl":1,"end":[0,1],"energi":1,"entri":1,"entrypoint":0,"equival":1,"error":1,"error_cod":1,"error_color":[],"especi":1,"europ":1,"ever":[],"exampl":[0,1],"exclus":1,"execut":1,"expect":1,"explan":[],"explicit":1,"explicit_local":1,"external_first":1,"external_id":1,"external_second":1,"extra_inform":1,"extract":1,"fals":[],"fig":[0,1],"figur":[0,1],"find":[],"first":1,"fit":[],"flag":[],"follow":[0,1],"format":1,"four":[],"frontend":1,"full_titl":0,"function":0,"fuseki":1,"g":1,"generat":1,"german":1,"get":1,"get_and_pad_start_and_end_timestamp":[],"get_colors_from_plot_target_set":[],"get_display_nam":1,"get_local":[0,1],"get_measur":1,"get_metric_info":1,"get_nam":1,"get_queried_interv":[0,1],"get_series_display_nam":1,"get_series_info":1,"get_series_measur":1,"get_series_nam":[0,1],"get_series_short_display_nam":1,"get_series_unit":[0,1],"get_short_display_nam":1,"get_unit":1,"get_value_dimension_info":1,"get_y_axis_label":[],"given":1,"global":1,"glom":1,"go":[0,1],"goal":[],"good":1,"graph_object":[0,1],"grid":[],"grid_color":[],"half":[],"handl":1,"happen":1,"hardwir":[],"helper":0,"helperexcept":1,"hetida":[0,1],"hide":[],"hide_legend":[],"hide_x_titl":[],"higher":[],"id":1,"import":[0,1],"includ":1,"index":[0,1],"infer":1,"info":1,"info_color":[],"inform":[],"input":[],"inspir":1,"instal":1,"instead":1,"insuffici":1,"insufficientplottingdata":1,"int":1,"integr":1,"intend":[],"interpret":1,"interv":1,"iso":1,"isoformat":1,"json":[0,1],"just":1,"keep":[],"key":1,"ki":[0,1],"kwarg":1,"l":[],"label":[0,1],"languag":1,"last":[],"layout":[],"leav":[],"legend":[],"let":[0,1],"level":[0,1],"light":1,"like":[0,1],"limit":0,"line":0,"line_color":[],"list":1,"local":1,"logo":[],"look":0,"m":1,"magnitud":[],"main":0,"make":[],"margin":[],"marker":0,"marker_symbol":[],"match":[],"meaning":1,"measur":1,"mention":[],"messag":1,"method":1,"metric":[0,1],"metric1":1,"metric2":1,"metric3":1,"metric_info":1,"metric_key":1,"might":0,"minim":[],"miss":[],"mode":[],"modifi":1,"modified_timezon":1,"modify_timezon":[0,1],"modifyng":[],"mtsf":1,"multipl":1,"multitsfram":1,"naiv":[],"name":[0,1],"name_of_metric1":1,"name_of_metric2":1,"name_of_series_1":1,"name_of_series_2":1,"necessari":[],"next":[],"non":1,"none":1,"normal":[],"note":1,"now":[],"object":1,"object_to_convert":1,"one":1,"onli":1,"oper":1,"option":1,"orang":1,"order":[],"otherwis":[],"output":1,"packag":1,"pad":[],"panda":1,"paper":[],"paper_bgcolor":[],"paramet":1,"pars":[],"pass":[],"pd":1,"place":1,"platform":1,"pleas":1,"plot":[],"plot_bgcolor":[],"plot_target_local":[],"plot_target_set":1,"plot_target_styl":[],"plot_target_timezon":[],"plotly_fig_to_json_dict":[],"point":[],"possibl":1,"potenti":[],"precis":[],"present":1,"print":1,"prioriti":[],"properti":[],"provid":1,"provis":0,"purpos":[],"pytz":1,"queri":1,"r":[],"rais":1,"rang":0,"rare":[],"re":1,"reason":[],"red":1,"ref_interval_end_timestamp":[0,1],"ref_interval_start_timestamp":[0,1],"reflect":[],"remov":[],"remove_plotly_bar":[],"remove_plotly_icon":[],"repres":0,"request":1,"resolv":1,"resolve_color":[0,1],"result":1,"retriev":[0,1],"return":[0,1],"rgba":[],"ruhr":1,"run":[],"s":[0,1],"said":[],"say":0,"scatter":[0,1],"scheme":1,"second":1,"see":1,"sensibl":1,"seri":[0,1],"serial":1,"set":[0,1],"set_agnostic_them":[0,1],"set_dt_tick":[0,1],"shade":1,"short":1,"short_display_nam":[0,1],"short_display_name_of_metric1":1,"short_display_name_of_seri":1,"show":1,"showlegend":[],"similar":1,"simple_whit":[],"sinc":1,"singl":1,"single_metric_dataset_metadata":0,"single_metric_metadata":[0,1],"size":[],"soon":[],"spec":1,"specif":[],"specifi":1,"standard":[0,1],"start":[0,1],"start_pad":[],"status":[],"status_color":[],"step":1,"str":1,"streamlin":1,"string":1,"structured_metadata":[0,1],"style":1,"success_color":[],"sure":[],"surpress":1,"t":[],"target":1,"tbd":1,"temp":1,"temperatur":1,"templat":1,"test":1,"theme":1,"theme_agnostic_templ":[],"tick":1,"tickformat":[],"time":1,"timeseri":[0,1],"timestamp":1,"timezon":[0,1],"titl":[],"title_standoff":[],"title_text":[],"to_datetim":1,"to_timezon":1,"toggleabl":1,"total_second":1,"trace":[],"transform":1,"tri":1,"true":1,"tupl":1,"turn":[],"type":1,"typeerror":1,"tz_convert":1,"underlying":1,"unit":[0,1],"unless":[],"updat":[],"update_layout":0,"update_x_axes_tickformat":[],"update_xax":0,"us":1,"use":1,"use_default_standoff":[],"use_minimum_margin":[],"use_muplot_axes_color":[],"use_muplot_grid":[],"use_muplot_line_and_mark":[],"use_platform_background":[],"use_platform_colorway":[],"use_platform_default":[],"use_simple_white_templ":[],"utc":1,"utcoffset":1,"valu":[0,1],"value_dim_1":1,"value_dim_info":1,"value_dimens":1,"value_name_of_seri":1,"valueerror":1,"vari":1,"variabl":1,"via":1,"visibl":[],"vision":0,"want":0,"warn_color":[],"water":0,"way":1,"well":1,"whether":1,"whose":1,"width":[],"will":1,"without":[],"write":1,"wwe":[],"x":[0,1],"xax":[],"y":[0,1],"yax":[],"yaxis_titl":0,"yield":1,"zone":1,"zoom":1,"\u00b5plot":[]},"titles":["First steps","hdhelpers"],"titleterms":{"Further":1,"How":0,"except":1,"explan":[],"first":0,"function":1,"get":0,"hdhelper":[0,1],"helper":1,"inform":1,"introduct":1,"metadata":[0,1],"plot":[0,1],"step":0,"tbd":0,"use":0}})
\ No newline at end of file
+Search.setIndex({"alltitles":{"First steps":[[0,null]],"Functions":[[1,"functions"]],"Further Information":[[1,"further-information"]],"How to get metadata with hdhelpers?":[[0,"how-to-get-metadata-with-hdhelpers"]],"How to use hdhelpers for plotting? (tbd)":[[0,"how-to-use-hdhelpers-for-plotting-tbd"]],"Introduction":[[1,"introduction"]],"exceptions":[[1,"module-hdhelpers.exceptions"]],"hdhelpers":[[1,null]],"helpers":[[1,"module-hdhelpers.helpers"]],"metadata":[[1,"module-hdhelpers.metadata"]],"plotting":[[1,"module-hdhelpers.plotting"]]},"docnames":["first_steps","index"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["first_steps.rst","index.rst"],"indexentries":{"get_display_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_display_names",false]],"get_locale() (in module hdhelpers.helpers)":[[1,"hdhelpers.helpers.get_locale",false]],"get_measurements() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_measurements",false]],"get_metric_info() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_metric_info",false]],"get_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_names",false]],"get_queried_interval() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_queried_interval",false]],"get_series_display_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_display_name",false]],"get_series_info() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_info",false]],"get_series_measurement() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_measurement",false]],"get_series_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_name",false]],"get_series_short_display_name() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_short_display_name",false]],"get_series_unit() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_series_unit",false]],"get_short_display_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_short_display_names",false]],"get_singlets_display_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_singlets_display_names",false]],"get_singlets_info() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_singlets_info",false]],"get_singlets_measurements() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_singlets_measurements",false]],"get_singlets_metric_info() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_singlets_metric_info",false]],"get_singlets_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_singlets_names",false]],"get_singlets_short_display_names() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_singlets_short_display_names",false]],"get_singlets_units() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_singlets_units",false]],"get_units() (in module hdhelpers.metadata)":[[1,"hdhelpers.metadata.get_units",false]],"hdhelpers.exceptions":[[1,"module-hdhelpers.exceptions",false]],"hdhelpers.helpers":[[1,"module-hdhelpers.helpers",false]],"hdhelpers.metadata":[[1,"module-hdhelpers.metadata",false]],"hdhelpers.plotting":[[1,"module-hdhelpers.plotting",false]],"helperexception":[[1,"hdhelpers.exceptions.HelperException",false]],"insufficientplottingdata":[[1,"hdhelpers.exceptions.InsufficientPlottingData",false]],"modify_timezone() (in module hdhelpers.helpers)":[[1,"hdhelpers.helpers.modify_timezone",false]],"module":[[1,"module-hdhelpers.exceptions",false],[1,"module-hdhelpers.helpers",false],[1,"module-hdhelpers.metadata",false],[1,"module-hdhelpers.plotting",false]],"resolve_color() (in module hdhelpers.plotting)":[[1,"hdhelpers.plotting.resolve_color",false]],"set_agnostic_theme() (in module hdhelpers.plotting)":[[1,"hdhelpers.plotting.set_agnostic_theme",false]],"set_dt_ticks() (in module hdhelpers.plotting)":[[1,"hdhelpers.plotting.set_dt_ticks",false]]},"objects":{"hdhelpers":[[1,0,0,"-","exceptions"],[1,0,0,"-","helpers"],[1,0,0,"-","metadata"],[1,0,0,"-","plotting"]],"hdhelpers.exceptions":[[1,1,1,"","HelperException"],[1,1,1,"","InsufficientPlottingData"]],"hdhelpers.helpers":[[1,2,1,"","get_locale"],[1,2,1,"","modify_timezone"]],"hdhelpers.metadata":[[1,2,1,"","get_display_names"],[1,2,1,"","get_measurements"],[1,2,1,"","get_metric_info"],[1,2,1,"","get_names"],[1,2,1,"","get_queried_interval"],[1,2,1,"","get_series_display_name"],[1,2,1,"","get_series_info"],[1,2,1,"","get_series_measurement"],[1,2,1,"","get_series_name"],[1,2,1,"","get_series_short_display_name"],[1,2,1,"","get_series_unit"],[1,2,1,"","get_short_display_names"],[1,2,1,"","get_singlets_display_names"],[1,2,1,"","get_singlets_info"],[1,2,1,"","get_singlets_measurements"],[1,2,1,"","get_singlets_metric_info"],[1,2,1,"","get_singlets_names"],[1,2,1,"","get_singlets_short_display_names"],[1,2,1,"","get_singlets_units"],[1,2,1,"","get_units"]],"hdhelpers.plotting":[[1,2,1,"","resolve_color"],[1,2,1,"","set_agnostic_theme"],[1,2,1,"","set_dt_ticks"]]},"objnames":{"0":["py","module","Python module"],"1":["py","exception","Python exception"],"2":["py","function","Python function"]},"objtypes":{"0":"py:module","1":"py:exception","2":"py:function"},"terms":{"000z":0,"00z":1,"01t01":1,"01t08":0,"05t13":1,"06t13":1,"A":1,"DO":0,"For":[0,1],"How":1,"If":1,"In":[0,1],"It":1,"NOT":0,"Our":0,"The":1,"This":1,"To":1,"We":0,"__data__":0,"__hd_wrapped_data_object__":0,"__metadata__":0,"abc":1,"abov":0,"absent":1,"abstract":1,"access":1,"accord":0,"adequ":1,"adit":1,"adjust":1,"agnost":1,"all_timezon":1,"allow":1,"ambigu":1,"ani":1,"anoth":1,"applic":1,"arbitrari":1,"arbitrarili":1,"arg":1,"associ":1,"assum":1,"attr":1,"avail":1,"axe":1,"axi":0,"back":1,"background":1,"base":1,"behaviour":1,"berlin":1,"bool":1,"boolean":1,"by_metr":1,"c":1,"can":[0,1],"carri":1,"case":1,"certain":1,"class":1,"cm":0,"code":[0,1],"collect":1,"color":[0,1],"color_str":1,"column":1,"column_nam":1,"compli":1,"compon":[0,1],"concret":1,"contain":1,"context":1,"contrast":1,"control":1,"convent":1,"convert":1,"convert_index":1,"correspond":[0,1],"counterpart":1,"custom":1,"dark":1,"data":1,"datafram":1,"dataset_metadata":1,"date":1,"datetim":1,"de":1,"de_d":1,"def":0,"default":1,"default_titl":0,"defaultdict":1,"defin":[0,1],"design":[0,1],"determin":1,"dict":1,"dictionari":1,"dimens":1,"direct":[0,1],"display":1,"display_nam":1,"display_name_of_metric1":1,"display_name_of_seri":1,"e":1,"ea1255":1,"eas":1,"eb6962":1,"edit":0,"empti":1,"en":1,"enabl":1,"end":[0,1],"energi":1,"entri":1,"entrypoint":0,"equival":1,"error":1,"error_cod":1,"especi":1,"europ":1,"exact":1,"exampl":[0,1],"exclus":1,"execut":1,"expect":1,"explicit":1,"explicit_local":1,"external_first":1,"external_id":1,"external_second":1,"extra_inform":1,"extract":1,"fall":1,"fig":[0,1],"figur":[0,1],"first":1,"follow":[0,1],"format":1,"frontend":1,"full_titl":0,"function":0,"fuseki":1,"g":1,"generat":1,"german":1,"get":1,"get_display_nam":1,"get_local":[0,1],"get_measur":1,"get_metric_info":1,"get_nam":1,"get_queried_interv":[0,1],"get_series_display_nam":1,"get_series_info":1,"get_series_measur":1,"get_series_nam":[0,1],"get_series_short_display_nam":1,"get_series_unit":[0,1],"get_short_display_nam":1,"get_singlets_display_nam":1,"get_singlets_info":1,"get_singlets_measur":1,"get_singlets_metric_info":1,"get_singlets_nam":1,"get_singlets_short_display_nam":1,"get_singlets_unit":1,"get_unit":1,"get_value_dimension_info":1,"given":1,"global":1,"glom":1,"go":[0,1],"good":1,"graph_object":[0,1],"handl":1,"happen":1,"helper":0,"helperexcept":1,"hetida":[0,1],"hold":1,"id":1,"identifi":1,"import":[0,1],"includ":1,"index":[0,1],"infer":1,"info":1,"inspir":1,"instal":1,"instead":1,"insuffici":1,"insufficientplottingdata":1,"int":1,"integr":1,"interpret":1,"interv":1,"iso":1,"isoformat":1,"json":[0,1],"just":1,"key":1,"ki":[0,1],"kwarg":1,"label":[0,1],"languag":1,"let":[0,1],"level":[0,1],"light":1,"like":[0,1],"limit":0,"line":0,"list":1,"local":1,"look":0,"m":1,"main":0,"mani":1,"marker":0,"meaning":1,"measur":1,"messag":1,"method":1,"metric":[0,1],"metric1":1,"metric2":1,"metric3":1,"metric_info":1,"metric_key":1,"might":0,"miss":1,"modifi":1,"modified_timezon":1,"modify_timezon":[0,1],"mtsf":1,"multipl":1,"multitsfram":1,"name":[0,1],"name_of_metric1":1,"name_of_metric2":1,"name_of_series_1":1,"name_of_series_2":1,"neither":1,"non":1,"none":1,"note":1,"object":1,"object_to_convert":1,"one":1,"onli":1,"oper":1,"option":1,"orang":1,"output":1,"packag":1,"panda":1,"paramet":1,"pd":1,"place":1,"platform":1,"pleas":1,"plot_target_set":1,"possibl":1,"present":1,"print":1,"provid":1,"provis":0,"pytz":1,"queri":1,"rais":1,"rang":0,"re":1,"red":1,"ref_interval_end_timestamp":[0,1],"ref_interval_start_timestamp":[0,1],"repres":0,"request":1,"resolv":1,"resolve_color":[0,1],"respect":1,"result":1,"retriev":[0,1],"return":[0,1],"ruhr":1,"s":[0,1],"say":0,"scatter":[0,1],"scheme":1,"second":1,"see":1,"sensibl":1,"seri":[0,1],"serial":1,"set":[0,1],"set_agnostic_them":[0,1],"set_dt_tick":[0,1],"shade":1,"short":1,"short_display_nam":[0,1],"short_display_name_of_metric1":1,"short_display_name_of_seri":1,"show":1,"similar":1,"sinc":1,"singl":1,"single_metr":1,"single_metric_dataset_metadata":0,"single_metric_metadata":[0,1],"singletsfram":1,"spec":1,"specifi":1,"standard":[0,1],"start":[0,1],"state":1,"step":1,"str":1,"streamlin":1,"string":1,"structured_metadata":[0,1],"stsf":1,"style":1,"surpress":1,"t":1,"target":1,"tbd":1,"temp":1,"temperatur":1,"templat":1,"test":1,"theme":1,"therefor":1,"tick":1,"time":1,"timeseri":[0,1],"timestamp":1,"timezon":[0,1],"to_datetim":1,"to_timezon":1,"toggleabl":1,"total_second":1,"transform":1,"tri":1,"true":1,"tupl":1,"type":1,"typeerror":1,"tz_convert":1,"underlying":1,"unit":[0,1],"unknown":1,"update_layout":0,"update_xax":0,"us":1,"use":1,"utc":1,"utcoffset":1,"valu":[0,1],"value_dim_1":1,"value_dim_info":1,"value_dimens":1,"value_dimensions_shar":1,"value_name_of_seri":1,"valueerror":1,"vari":1,"variabl":1,"via":1,"vision":0,"want":0,"water":0,"way":1,"well":1,"whether":1,"whose":1,"will":1,"without":1,"write":1,"x":[0,1],"y":[0,1],"yaxis_titl":0,"yield":1,"zone":1,"zoom":1},"titles":["First steps","hdhelpers"],"titleterms":{"Further":1,"How":0,"except":1,"first":0,"function":1,"get":0,"hdhelper":[0,1],"helper":1,"inform":1,"introduct":1,"metadata":[0,1],"plot":[0,1],"step":0,"tbd":0,"use":0}})
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 5d30c31..f9f52e0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -18,7 +18,7 @@ exclude = [
[project]
name = "hdhelpers"
-version = "0.0.8"
+version = "0.0.9"
description = "Streamlines metadata & timezone handling, and plotting in hetida designer components"
readme = "README.md"
maintainers = [
diff --git a/src/hdhelpers/__init__.py b/src/hdhelpers/__init__.py
index cfd9837..40f1073 100644
--- a/src/hdhelpers/__init__.py
+++ b/src/hdhelpers/__init__.py
@@ -10,7 +10,7 @@
from hdhelpers.plotting.timestamp_ticks import TICKFORMATSTOPS, set_dt_ticks
# do not edit line of __version__ as it is automatically modified by running ./run build_package
-__version__ = "0.0.8"
+__version__ = "0.0.9"
# import can be done by using `from hdhelpers import *``
__all__ = [
diff --git a/src/hdhelpers/metadata/__init__.py b/src/hdhelpers/metadata/__init__.py
index db39ed9..3b0a1b4 100644
--- a/src/hdhelpers/metadata/__init__.py
+++ b/src/hdhelpers/metadata/__init__.py
@@ -15,6 +15,13 @@
get_series_short_display_name,
get_series_unit,
get_short_display_names,
+ get_singlets_display_names,
+ get_singlets_info,
+ get_singlets_measurements,
+ get_singlets_metric_info,
+ get_singlets_names,
+ get_singlets_short_display_names,
+ get_singlets_units,
get_units,
)
@@ -32,4 +39,11 @@
"get_measurements",
"get_metric_info",
"get_units",
+ "get_singlets_info",
+ "get_singlets_names",
+ "get_singlets_short_display_names",
+ "get_singlets_display_names",
+ "get_singlets_measurements",
+ "get_singlets_metric_info",
+ "get_singlets_units",
]
diff --git a/src/hdhelpers/metadata/_helpers.py b/src/hdhelpers/metadata/_helpers.py
index 384a0af..0120981 100644
--- a/src/hdhelpers/metadata/_helpers.py
+++ b/src/hdhelpers/metadata/_helpers.py
@@ -2,7 +2,7 @@
from typing import Any
import pandas as pd
-from glom import Check, Coalesce, Spec, glom
+from glom import Check, Coalesce, GlomError, Spec, glom
import hdhelpers.metadata._specs as _specs
@@ -61,3 +61,62 @@ def get_value_dimension_info(
return defaultdict(
lambda: defaultdict(lambda: None), value_dimension_info_by_metric_by_value_dimension
)
+
+
+_SINGLE_METRIC_KEY = "single_metric"
+
+
+def _select_single_metric(info_by_metric: dict, attrs: Any, empty: Any) -> Any:
+ """Pick the entry of the one metric of a SingleTSFrame out of a by-metric mapping
+
+ The single metric is identified via _SINGLE_METRIC_KEY in "dataset_metadata". If that is
+ missing but exactly one metric is present, that one is used, since a SingleTSFrame cannot
+ be ambiguous in this respect. Otherwise `empty` is returned.
+ """
+ metric = extract_from_metadata(attrs, key=_SINGLE_METRIC_KEY)
+ if metric is not None and metric in info_by_metric:
+ return info_by_metric[metric]
+
+ if len(info_by_metric) == 1:
+ return next(iter(info_by_metric.values()))
+
+ return empty
+
+
+def singlets_value_dimension_info(
+ singletsframe: pd.DataFrame, value_dim_info: str | Spec
+) -> defaultdict[str, Any]:
+ """Metadata info of the value dimensions of the single metric of a SingleTSFrame
+
+ A SingleTSFrame holds exactly one metric but - like a MultiTSFrame - arbitrarily many
+ value dimensions. So in contrast to get_value_dimension_info (which is keyed by metric
+ first) this returns a defaultdict keyed by value dimension column name only.
+
+ The single metric is identified via "dataset_metadata.single_metric". If that is missing
+ but the metadata contains exactly one metric, that metric is used, since a SingleTSFrame
+ cannot be ambiguous in this respect.
+
+ What is extracted per value dimension is exactly what get_value_dimension_info extracts -
+ only the metric level is collapsed away.
+ """
+ return _select_single_metric(
+ get_value_dimension_info(singletsframe, value_dim_info),
+ singletsframe.attrs,
+ empty=defaultdict(lambda: None),
+ )
+
+
+def singlets_metric_info(singletsframe: pd.DataFrame, metric_info: str | Spec) -> Any:
+ """Metadata of the single metric of a SingleTSFrame
+
+ Counterpart of get_metric_info for SingleTSFrames: instead of a mapping keyed by metric
+ this directly returns the requested information for the one metric, or None if it cannot
+ be determined - which includes the case of absent metric metadata, since components should
+ not require metadata (see the metadata conventions documentation).
+ """
+ try:
+ info_by_metric = glom(singletsframe.attrs, _specs.spec_by_metric_key(metric_info))
+ except GlomError: # no metric metadata present at all
+ return None
+
+ return _select_single_metric(info_by_metric, singletsframe.attrs, empty=None)
diff --git a/src/hdhelpers/metadata/_specs.py b/src/hdhelpers/metadata/_specs.py
index 00e6d52..e5749bf 100644
--- a/src/hdhelpers/metadata/_specs.py
+++ b/src/hdhelpers/metadata/_specs.py
@@ -3,6 +3,15 @@
from glom import A, Check, Coalesce, Iter, Merge, S, Spec, T
+# The metadatum specs of the individual accessors, including their fallback chains.
+# Defined once here and shared by the MultiTSFrame and the SingleTSFrame accessors, which
+# only differ in whether the result is keyed by metric first - not in what they extract.
+UNIT: Spec = "unit"
+MEASUREMENT: Spec = "measurement"
+NAME: Spec = Coalesce("name", default=None)
+DISPLAY_NAME: Spec = Coalesce("display_name", "name", default=None)
+SHORT_DISPLAY_NAME: Spec = Coalesce("short_display_name", "display_name", "name", default=None)
+
def by_metric_key_by_val_dimension(metadatum_key: str | Spec) -> Spec:
"""Providesglom spec that extracts a metadatum by metric by value dimension
@@ -33,7 +42,14 @@ def by_metric_key_by_val_dimension(metadatum_key: str | Spec) -> Spec:
def _spec_metric_key() -> Spec:
- return ("dataset_metadata.metric_key", A.globals.metric_key)
+ """Which key of the metric objects identifies a metric
+
+ Per the metadata conventions "metric_key" is optional and defaults to "id". Note that
+ it therefore cannot act as the discriminator between the current and the older metadata
+ conventions - that role belongs to "metrics" being a list, see
+ _spec_actual_per_metric_per_value_dimensions.
+ """
+ return (Coalesce("dataset_metadata.metric_key", default="id"), A.globals.metric_key)
def _spec_defaults_by_value_dimension(metadatum_key: str | Spec) -> Spec:
@@ -65,24 +81,27 @@ def _spec_defaults_by_metric(metadatum_key: str | Spec) -> Spec:
def _spec_actual_per_metric_per_value_dimensions(metadatum_key: str | Spec) -> Spec:
- return Coalesce(
- (
- "metrics",
- Check(instance_of=list),
- _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result(
- S.globals.metric_key,
- (
- Coalesce("value_dimensions", default={}),
- _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result(
- "column",
- Coalesce(metadatum_key, default=None),
- add_keys_with_none_values=["value"],
- ),
+ """Deliberately without a default: this is what discriminates the metadata conventions
+
+ If "metrics" is absent or is not a list, this spec fails, which makes the enclosing
+ _spec_new_convention fail, so that by_metric_key_by_val_dimension falls through to the
+ older conventions.
+ """
+ return (
+ "metrics",
+ Check(instance_of=list),
+ _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result(
+ S.globals.metric_key,
+ (
+ Coalesce("value_dimensions", default={}),
+ _build_dict_from_iterable_from_key_and_subspec_and_then_proceed_on_result(
+ "column",
+ Coalesce(metadatum_key, default=None),
+ add_keys_with_none_values=["value"],
),
- key_as_value=True,
),
+ key_as_value=True,
),
- default={},
)
@@ -199,7 +218,7 @@ def spec_by_metric_key(metadatum_key: str | Spec) -> Spec:
return Coalesce(
( # current metdadata convention
{
- "metric_key": ("dataset_metadata.metric_key", A.globals.metric_key),
+ "metric_key": _spec_metric_key(),
"by_metric": (
"metrics",
Check(instance_of=list),
diff --git a/src/hdhelpers/metadata/helpers.py b/src/hdhelpers/metadata/helpers.py
index f9ed116..d0ff0c4 100644
--- a/src/hdhelpers/metadata/helpers.py
+++ b/src/hdhelpers/metadata/helpers.py
@@ -22,8 +22,11 @@
check_series_or_dataframe,
extract_from_metadata,
get_value_dimension_info,
+ singlets_metric_info,
+ singlets_value_dimension_info,
spec_not_none,
)
+import hdhelpers.metadata._specs as _specs
from hdhelpers.metadata._specs import spec_by_metric_key
@@ -75,7 +78,7 @@ def get_units(
"""
check_dataframe(multitsframe)
- return get_value_dimension_info(multitsframe, "unit")
+ return get_value_dimension_info(multitsframe, _specs.UNIT)
def get_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, str | None]]:
@@ -120,7 +123,7 @@ def get_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, s
"""
check_dataframe(multitsframe)
- return get_value_dimension_info(multitsframe, Coalesce("name", default=None))
+ return get_value_dimension_info(multitsframe, _specs.NAME)
def get_display_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, str | None]]:
@@ -166,7 +169,7 @@ def get_display_names(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdic
"""
check_dataframe(multitsframe)
- return get_value_dimension_info(multitsframe, Coalesce("display_name", "name", default=None))
+ return get_value_dimension_info(multitsframe, _specs.DISPLAY_NAME)
def get_short_display_names(
@@ -217,9 +220,7 @@ def get_short_display_names(
"""
check_dataframe(multitsframe)
- return get_value_dimension_info(
- multitsframe, Coalesce("short_display_name", "display_name", "name", default=None)
- )
+ return get_value_dimension_info(multitsframe, _specs.SHORT_DISPLAY_NAME)
def get_measurements(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict[str, str | None]]:
@@ -249,7 +250,7 @@ def get_measurements(multitsframe: pd.DataFrame) -> defaultdict[str, defaultdict
"""
check_dataframe(multitsframe)
- return get_value_dimension_info(multitsframe, "measurement")
+ return get_value_dimension_info(multitsframe, _specs.MEASUREMENT)
def get_metric_info(multitsframe: pd.DataFrame, metric_info: str | Spec) -> defaultdict[str, Any]:
@@ -567,6 +568,309 @@ def get_series_measurement(series: pd.Series) -> str | None:
return cast(str | None, get_series_info(series, "measurement"))
+def get_singlets_info(singletsframe: pd.DataFrame, value_dim_info: str | Spec) -> Any:
+ """Gets an arbitrary info of the value dimensions of a SingleTSFrame
+
+ A SingleTSFrame (STSF) holds exactly one metric - like a Series - but arbitrarily many
+ value dimensions - like a MultiTSFrame. Therefore, in contrast to the MTSF functions,
+ the result is keyed by value dimension only and not by metric first.
+
+ The single metric is identified via "single_metric" in "dataset_metadata". If that is
+ missing but the metadata contains exactly one metric, that metric is used, since a STSF
+ cannot be ambiguous in this respect.
+
+ Args:
+ singletsframe (pd.DataFrame): STSF with metadata following the convention.
+ value_dim_info (str | Spec): Name of information to retrieve. Note that `value_dim_info` is interpreted as a glom Spec.
+
+ Returns:
+ defaultdict[str, Any]: Dictionary of value dimensions containing the retrieved information. If the information is not present for a value dimension the corresponding value is None.
+
+ Raises:
+ TypeError: If `singletsframe` is not a DataFrame.
+
+ Code example:
+
+ .. doctest:: metadata.get_singlets_info
+
+ >>> dataframe.attrs = {
+ ... "dataset_metadata": {"single_metric": "abc.temp", "metric_key": "external_id"},
+ ... "metrics": [
+ ... {
+ ... "external_id": "abc.temp",
+ ... "value_dimensions": [
+ ... {"column": "value", "unit": "°C"},
+ ... {"column": "state"},
+ ... ],
+ ... }
+ ... ],
+ ... }
+ >>> result = get_singlets_info(dataframe, "unit")
+ >>> result["value"]
+ '°C'
+ >>> result["state"] is None
+ True
+ """
+
+ check_dataframe(singletsframe)
+ return singlets_value_dimension_info(singletsframe, value_dim_info)
+
+
+def get_singlets_units(singletsframe: pd.DataFrame) -> defaultdict[str, str | None]:
+ """Gets units of the value dimensions of a SingleTSFrame from metadata
+
+ Args:
+ singletsframe (pd.DataFrame): STSF with metadata following the convention.
+
+ Returns:
+ defaultdict[str, str | None]: Dictionary of value dimensions containing the units. If the unit is not present for a value dimension the corresponding value is None.
+
+ Raises:
+ TypeError: If `singletsframe` is not a DataFrame.
+
+ Note that a value dimension falls back to "value_dimensions_shared" if it carries no unit
+ of its own:
+
+ .. doctest:: metadata.get_singlets_units
+
+ >>> dataframe.attrs = {
+ ... "dataset_metadata": {"single_metric": "abc.temp", "metric_key": "external_id"},
+ ... "metrics": [
+ ... {
+ ... "external_id": "abc.temp",
+ ... "value_dimensions": [
+ ... {"column": "value", "unit": "°C"},
+ ... {"column": "state"},
+ ... ],
+ ... }
+ ... ],
+ ... "value_dimensions_shared": [{"column": "state", "unit": "UNKNOWN"}],
+ ... }
+ >>> result = get_singlets_units(dataframe)
+ >>> result["value"]
+ '°C'
+ >>> result["state"]
+ 'UNKNOWN'
+ >>> result["not-given"] is None
+ True
+
+ The "metric_key" is optional and defaults to "id":
+
+ .. doctest:: metadata.get_singlets_units
+
+ >>> dataframe.attrs = {
+ ... "dataset_metadata": {"single_metric": "abc.temp"},
+ ... "metrics": [
+ ... {"id": "abc.temp", "value_dimensions": [{"column": "value", "unit": "m"}]}
+ ... ],
+ ... }
+ >>> get_singlets_units(dataframe)["value"]
+ 'm'
+ """
+
+ check_dataframe(singletsframe)
+ return singlets_value_dimension_info(singletsframe, _specs.UNIT)
+
+
+def get_singlets_names(singletsframe: pd.DataFrame) -> defaultdict[str, str | None]:
+ """Gets names of the value dimensions of a SingleTSFrame from metadata
+
+ Args:
+ singletsframe (pd.DataFrame): STSF with metadata following the convention.
+
+ Returns:
+ defaultdict[str, str | None]: Dictionary of value dimensions containing the names. If the name is not present for a value dimension the corresponding value is None.
+
+ Raises:
+ TypeError: If `singletsframe` is not a DataFrame.
+
+ Code example:
+
+ .. doctest:: metadata.get_singlets_names
+
+ >>> dataframe.attrs = {
+ ... "dataset_metadata": {"single_metric": "abc.temp", "metric_key": "external_id"},
+ ... "metrics": [
+ ... {
+ ... "external_id": "abc.temp",
+ ... "value_dimensions": [
+ ... {"column": "value", "name": "temperature"},
+ ... {"column": "state"},
+ ... ],
+ ... }
+ ... ],
+ ... }
+ >>> result = get_singlets_names(dataframe)
+ >>> result["value"]
+ 'temperature'
+ >>> result["state"] is None
+ True
+ """
+
+ check_dataframe(singletsframe)
+ return singlets_value_dimension_info(singletsframe, _specs.NAME)
+
+
+def get_singlets_display_names(singletsframe: pd.DataFrame) -> defaultdict[str, str | None]:
+ """Gets display names of the value dimensions of a SingleTSFrame from metadata
+
+ Falls back to the name of the value dimension if no display name is given.
+
+ Args:
+ singletsframe (pd.DataFrame): STSF with metadata following the convention.
+
+ Returns:
+ defaultdict[str, str | None]: Dictionary of value dimensions containing the display names. If neither display name nor name is present for a value dimension the corresponding value is None.
+
+ Raises:
+ TypeError: If `singletsframe` is not a DataFrame.
+
+ Code example:
+
+ .. doctest:: metadata.get_singlets_display_names
+
+ >>> dataframe.attrs = {
+ ... "dataset_metadata": {"single_metric": "abc.temp", "metric_key": "external_id"},
+ ... "metrics": [
+ ... {
+ ... "external_id": "abc.temp",
+ ... "value_dimensions": [
+ ... {"column": "value", "name": "temperature", "display_name": "temp"},
+ ... {"column": "state", "name": "measurement state"},
+ ... ],
+ ... }
+ ... ],
+ ... }
+ >>> result = get_singlets_display_names(dataframe)
+ >>> result["value"]
+ 'temp'
+ >>> result["state"]
+ 'measurement state'
+ """
+
+ check_dataframe(singletsframe)
+ return singlets_value_dimension_info(singletsframe, _specs.DISPLAY_NAME)
+
+
+def get_singlets_short_display_names(singletsframe: pd.DataFrame) -> defaultdict[str, str | None]:
+ """Gets short display names of the value dimensions of a SingleTSFrame from metadata
+
+ Falls back to the display name and then to the name of the value dimension if no short
+ display name is given.
+
+ Args:
+ singletsframe (pd.DataFrame): STSF with metadata following the convention.
+
+ Returns:
+ defaultdict[str, str | None]: Dictionary of value dimensions containing the short display names. If none of short display name, display name and name is present for a value dimension the corresponding value is None.
+
+ Raises:
+ TypeError: If `singletsframe` is not a DataFrame.
+
+ Code example:
+
+ .. doctest:: metadata.get_singlets_short_display_names
+
+ >>> dataframe.attrs = {
+ ... "dataset_metadata": {"single_metric": "abc.temp", "metric_key": "external_id"},
+ ... "metrics": [
+ ... {
+ ... "external_id": "abc.temp",
+ ... "value_dimensions": [
+ ... {"column": "value", "name": "temperature", "short_display_name": "T"},
+ ... {"column": "state", "display_name": "state"},
+ ... ],
+ ... }
+ ... ],
+ ... }
+ >>> result = get_singlets_short_display_names(dataframe)
+ >>> result["value"]
+ 'T'
+ >>> result["state"]
+ 'state'
+ """
+
+ check_dataframe(singletsframe)
+ return singlets_value_dimension_info(singletsframe, _specs.SHORT_DISPLAY_NAME)
+
+
+def get_singlets_measurements(singletsframe: pd.DataFrame) -> defaultdict[str, str | None]:
+ """Gets measurements (types) of the value dimensions of a SingleTSFrame from metadata
+
+ Args:
+ singletsframe (pd.DataFrame): STSF with metadata following the convention.
+
+ Returns:
+ defaultdict[str, str | None]: Dictionary of value dimensions containing the measurements. If "measurement" is not given for a value dimension the corresponding value is None.
+
+ Raises:
+ TypeError: If `singletsframe` is not a DataFrame.
+
+ Code example:
+
+ .. doctest:: metadata.get_singlets_measurements
+
+ >>> dataframe.attrs = {
+ ... "dataset_metadata": {"single_metric": "abc.temp", "metric_key": "external_id"},
+ ... "metrics": [
+ ... {
+ ... "external_id": "abc.temp",
+ ... "value_dimensions": [
+ ... {"column": "value", "measurement": "temperature"},
+ ... ],
+ ... }
+ ... ],
+ ... }
+ >>> get_singlets_measurements(dataframe)["value"]
+ 'temperature'
+ """
+
+ check_dataframe(singletsframe)
+ return singlets_value_dimension_info(singletsframe, _specs.MEASUREMENT)
+
+
+def get_singlets_metric_info(singletsframe: pd.DataFrame, metric_info: str | Spec) -> Any:
+ """Gets metadata associated to the single metric of a SingleTSFrame
+
+ Counterpart of :func:`get_metric_info` for SingleTSFrames: instead of a dictionary keyed
+ by metric this directly returns the requested information for the one metric.
+
+ Args:
+ singletsframe (pd.DataFrame): STSF with metadata following the convention.
+ metric_info (str | Spec): Name of information to retrieve. Note that `metric_info` is interpreted as a glom Spec.
+
+ Returns:
+ Any: Retrieved information defined by `metric_info`, or None if it cannot be determined - which includes the case of absent metric metadata.
+
+ Raises:
+ TypeError: If `singletsframe` is not a DataFrame.
+
+ Code example:
+
+ .. doctest:: metadata.get_singlets_metric_info
+
+ >>> dataframe.attrs = {
+ ... "dataset_metadata": {"single_metric": "abc.temp", "metric_key": "external_id"},
+ ... "metrics": [{"external_id": "abc.temp", "name": "ABC temperature"}],
+ ... }
+ >>> get_singlets_metric_info(dataframe, "name")
+ 'ABC temperature'
+ >>> get_singlets_metric_info(dataframe, "not-given") is None
+ True
+
+ Without any metadata None is returned:
+
+ .. doctest:: metadata.get_singlets_metric_info
+
+ >>> dataframe.attrs = {}
+ >>> get_singlets_metric_info(dataframe, "name") is None
+ True
+ """
+
+ check_dataframe(singletsframe)
+ return singlets_metric_info(singletsframe, metric_info)
+
+
def get_queried_interval(
data: pd.Series | pd.DataFrame,
) -> tuple[datetime.datetime | None, datetime.datetime | None]:
diff --git a/tests/conftest.py b/tests/conftest.py
index 90da099..3d2aac5 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -11,4 +11,5 @@
empty_series_with_attr,
empty_series_with_old_attr,
empty_series_with_old_attr_real,
+ empty_singletsframe_with_attr,
)
diff --git a/tests/data/fixtures/metadata.py b/tests/data/fixtures/metadata.py
index 4569b6a..88b9a90 100644
--- a/tests/data/fixtures/metadata.py
+++ b/tests/data/fixtures/metadata.py
@@ -34,6 +34,16 @@ def empty_mts_with_old_attr_real() -> pd.DataFrame:
return empty_mts
+@pytest.fixture(scope="function")
+def empty_singletsframe_with_attr() -> pd.DataFrame:
+ with open("tests/data/json_templates/singletsframe_attrs.json", "r") as file:
+ metadata_for_sts = json.load(file)
+
+ empty_sts = pd.DataFrame()
+ empty_sts.attrs = metadata_for_sts
+ return empty_sts
+
+
@pytest.fixture(scope="function")
def empty_series_with_old_attr_real() -> pd.Series:
with open("tests/data/json_templates/series_attrs_old_real.json", "r") as file:
diff --git a/tests/data/json_templates/singletsframe_attrs.json b/tests/data/json_templates/singletsframe_attrs.json
new file mode 100644
index 0000000..1192ff2
--- /dev/null
+++ b/tests/data/json_templates/singletsframe_attrs.json
@@ -0,0 +1,29 @@
+{
+ "dataset_metadata": {
+ "single_metric": "abc.temp",
+ "metric_key": "external_id"
+ },
+ "metrics": [
+ {
+ "external_id": "abc.temp",
+ "name": "ABC temperature",
+ "value_dimensions": [
+ {
+ "column": "value",
+ "name": "temperature",
+ "unit": "°C"
+ },
+ {
+ "column": "state",
+ "name": "measurement state"
+ }
+ ]
+ }
+ ],
+ "value_dimensions_shared": [
+ {
+ "column": "state",
+ "unit": "UNKNOWN"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/metadata/test_metadata.py b/tests/metadata/test_metadata.py
index ca9c94d..d09a127 100644
--- a/tests/metadata/test_metadata.py
+++ b/tests/metadata/test_metadata.py
@@ -14,6 +14,13 @@
get_series_short_display_name,
get_series_unit,
# get_units,
+ get_singlets_display_names,
+ get_singlets_info,
+ get_singlets_measurements,
+ get_singlets_metric_info,
+ get_singlets_names,
+ get_singlets_short_display_names,
+ get_singlets_units,
)
@@ -381,3 +388,116 @@ def test_get_names_for_mts_with_metric_info_fallback_option():
result_short_display_name = get_short_display_names(dataframe)
assert result_name == result_display_name == result_short_display_name
+
+
+def test_get_singlets_info_is_keyed_by_value_dimension_only(empty_singletsframe_with_attr):
+ """A SingleTSFrame has one metric, so no metric level in the result"""
+ units = get_singlets_units(empty_singletsframe_with_attr)
+
+ assert units["value"] == "°C"
+ assert units["state"] == "UNKNOWN" # falls back to value_dimensions_shared
+ assert units["not-given"] is None
+
+
+def test_get_singlets_names(empty_singletsframe_with_attr):
+ names = get_singlets_names(empty_singletsframe_with_attr)
+
+ assert names["value"] == "temperature"
+ assert names["state"] == "measurement state"
+ assert names["not-given"] is None
+
+
+def test_get_singlets_display_names_fall_back_to_name(empty_singletsframe_with_attr):
+ empty_singletsframe_with_attr.attrs["metrics"][0]["value_dimensions"][0]["display_name"] = (
+ "temp"
+ )
+
+ display_names = get_singlets_display_names(empty_singletsframe_with_attr)
+
+ assert display_names["value"] == "temp"
+ assert display_names["state"] == "measurement state"
+
+
+def test_get_singlets_short_display_names_fall_back(empty_singletsframe_with_attr):
+ empty_singletsframe_with_attr.attrs["metrics"][0]["value_dimensions"][0][
+ "short_display_name"
+ ] = "T"
+ empty_singletsframe_with_attr.attrs["metrics"][0]["value_dimensions"][1]["display_name"] = (
+ "state"
+ )
+
+ short_display_names = get_singlets_short_display_names(empty_singletsframe_with_attr)
+
+ assert short_display_names["value"] == "T"
+ assert short_display_names["state"] == "state"
+
+
+def test_get_singlets_measurements(empty_singletsframe_with_attr):
+ empty_singletsframe_with_attr.attrs["metrics"][0]["value_dimensions"][0]["measurement"] = (
+ "temperature"
+ )
+
+ measurements = get_singlets_measurements(empty_singletsframe_with_attr)
+
+ assert measurements["value"] == "temperature"
+ assert measurements["state"] is None
+
+
+def test_get_singlets_info_with_arbitrary_spec(empty_singletsframe_with_attr):
+ assert get_singlets_info(empty_singletsframe_with_attr, "unit")["value"] == "°C"
+ assert get_singlets_info(empty_singletsframe_with_attr, "not-given")["value"] is None
+
+
+def test_get_singlets_metric_info(empty_singletsframe_with_attr):
+ assert get_singlets_metric_info(empty_singletsframe_with_attr, "name") == "ABC temperature"
+ assert get_singlets_metric_info(empty_singletsframe_with_attr, "external_id") == "abc.temp"
+ assert get_singlets_metric_info(empty_singletsframe_with_attr, "not-given") is None
+
+
+def test_get_singlets_metric_key_defaults_to_id():
+ """metric_key is optional per convention and defaults to "id" """
+ empty_singletsframe_with_attr = pd.DataFrame()
+ empty_singletsframe_with_attr.attrs = {
+ "dataset_metadata": {"single_metric": "abc.temp"},
+ "metrics": [{"id": "abc.temp", "value_dimensions": [{"column": "value", "unit": "m"}]}],
+ }
+
+ assert get_singlets_units(empty_singletsframe_with_attr)["value"] == "m"
+ assert get_singlets_metric_info(empty_singletsframe_with_attr, "id") == "abc.temp"
+
+
+def test_get_singlets_falls_back_to_only_metric_without_single_metric(
+ empty_singletsframe_with_attr,
+):
+ """If single_metric is missing but there is exactly one metric, use that one"""
+ del empty_singletsframe_with_attr.attrs["dataset_metadata"]["single_metric"]
+
+ assert get_singlets_units(empty_singletsframe_with_attr)["value"] == "°C"
+ assert get_singlets_metric_info(empty_singletsframe_with_attr, "name") == "ABC temperature"
+
+
+def test_get_singlets_without_metadata():
+ """Components should not require metadata, so no metadata must not raise"""
+ empty_singletsframe_with_attr = pd.DataFrame()
+
+ assert get_singlets_units(empty_singletsframe_with_attr)["value"] is None
+ assert get_singlets_names(empty_singletsframe_with_attr)["value"] is None
+ assert get_singlets_display_names(empty_singletsframe_with_attr)["value"] is None
+ assert get_singlets_short_display_names(empty_singletsframe_with_attr)["value"] is None
+ assert get_singlets_measurements(empty_singletsframe_with_attr)["value"] is None
+ assert get_singlets_metric_info(empty_singletsframe_with_attr, "name") is None
+
+
+@pytest.mark.parametrize(
+ "func",
+ [
+ get_singlets_units,
+ get_singlets_names,
+ get_singlets_display_names,
+ get_singlets_short_display_names,
+ get_singlets_measurements,
+ ],
+)
+def test_get_singlets_requires_a_dataframe(func):
+ with pytest.raises(TypeError):
+ func(pd.Series())
diff --git a/tests/metadata/test_metadata_migration.py b/tests/metadata/test_metadata_migration.py
index 25c477c..0363b50 100644
--- a/tests/metadata/test_metadata_migration.py
+++ b/tests/metadata/test_metadata_migration.py
@@ -1,3 +1,5 @@
+import pandas as pd
+
from hdhelpers.metadata import (
get_display_names,
get_measurements,
@@ -139,3 +141,55 @@ def test_series_unit_old(empty_series_with_old_attr):
assert get_series_short_display_name(empty_series_with_old_attr) == "muster"
assert get_series_measurement(empty_series_with_old_attr) is None
+
+
+def test_metric_key_defaults_to_id():
+ """ "metric_key" is optional per the conventions and defaults to "id"
+
+ It therefore must not act as the discriminator between the metadata conventions -
+ see test_by_metric_convention_stays_reachable_without_metric_key.
+ """
+ mts = pd.DataFrame()
+ mts.attrs = {
+ "dataset_metadata": {},
+ "metrics": [{"id": "metric1", "value_dimensions": [{"column": "value", "unit": "m³/s"}]}],
+ }
+
+ assert get_units(mts)["metric1"]["value"] == "m³/s"
+ assert get_metric_info(mts, "id")["metric1"] == "metric1"
+
+ # ... also with no "dataset_metadata" at all
+ mts.attrs = {
+ "metrics": [{"id": "metric1", "value_dimensions": [{"column": "value", "unit": "m³/s"}]}]
+ }
+
+ assert get_units(mts)["metric1"]["value"] == "m³/s"
+
+
+def test_by_metric_convention_stays_reachable_without_metric_key():
+ """The older "by_metric" convention must stay reachable
+
+ "metrics" being a list is what discriminates the current convention from the older ones,
+ so metadata without a "metrics" list still resolves via "by_metric".
+ """
+ mts = pd.DataFrame()
+ mts.attrs = {
+ "dataset_metadata": {},
+ "by_metric": {"metric1": {"value_dimensions": {"value": {"unit": "m³/s"}}}},
+ }
+
+ assert get_units(mts)["metric1"]["value"] == "m³/s"
+
+
+def test_metrics_list_takes_precedence_over_by_metric():
+ """If both are present the current convention wins"""
+ mts = pd.DataFrame()
+ mts.attrs = {
+ "dataset_metadata": {},
+ "metrics": [
+ {"id": "metric1", "value_dimensions": [{"column": "value", "unit": "FROM_NEW"}]}
+ ],
+ "by_metric": {"metric1": {"value_dimensions": {"value": {"unit": "FROM_OLD"}}}},
+ }
+
+ assert get_units(mts)["metric1"]["value"] == "FROM_NEW"
diff --git a/uv.lock b/uv.lock
index aaae1d5..815ed9d 100644
--- a/uv.lock
+++ b/uv.lock
@@ -270,7 +270,7 @@ wheels = [
[[package]]
name = "hdhelpers"
-version = "0.0.8"
+version = "0.0.9"
source = { editable = "." }
dependencies = [
{ name = "glom" },
|