diff --git a/PIconnect/AF.py b/PIconnect/AF.py index a4f354473..097bec943 100644 --- a/PIconnect/AF.py +++ b/PIconnect/AF.py @@ -32,9 +32,13 @@ def default_server(cls) -> dotnet.AF.PISystem | None: return None def __init__(self, server: str | None = None, database: str | None = None) -> None: + #: The PI AF server connection. self.server = self._initialise_server(server) + #: The PI AF database connection. self.database = self._initialise_database(database) - self.search = Search.Search(self.database) + #: Search reference for searching objects in the database. + #: See :class:`.Search.Search` for more information. + self.search: Search.Search = Search.Search(self.database) def _initialise_server(self, server: str | None) -> dotnet.AF.PISystem: """Initialise the server connection.""" @@ -151,7 +155,11 @@ def event_frames( class PIAFDatabase(AFDatabase): - """Context manager for connections to the PI Asset Framework database.""" + """Context manager for connections to the PI Asset Framework database. + + .. deprecated:: 1.0.0 + Use :class:`AFDatabase` instead. + """ version = "0.3.0" diff --git a/PIconnect/Asset.py b/PIconnect/Asset.py index 557aa4af3..c45377710 100644 --- a/PIconnect/Asset.py +++ b/PIconnect/Asset.py @@ -1,7 +1,7 @@ """Mirror of the OSISoft.AF.Asset namespace.""" import dataclasses -from typing import Generic, Self, TypeVar, overload +from typing import Generic, Self, TypeVar import pandas as pd # type: ignore @@ -10,11 +10,12 @@ __all__ = [ "AFDataReference", + "AFElement", + "AFElementList", "AFAttribute", "AFAttributeList", ] -T = TypeVar("T") ElementType = TypeVar("ElementType", bound=dotnet.AF.Asset.AFBaseElement) @@ -41,54 +42,6 @@ def pi_point(self) -> PI.PIPoint | None: return PI.PIPoint(self.data_reference.PIPoint) -class AFEnumerationValue: - """Representation of an AF enumeration value.""" - - def __init__(self, value: dotnet.AF.Asset.AFEnumerationValue) -> None: - self._value = value - - def __str__(self) -> str: - """Return the string representation of the enumeration value.""" - return self._value.Name - - def __int__(self) -> int: - """Return the integer representation of the enumeration value.""" - return self._value.Value - - def __repr__(self): - """Return the string representation of the enumeration value.""" - return f"{self.__class__.__qualname__}({self._value.Name})" - - @property - def name(self) -> str: - """Return the name of the enumeration value.""" - return self._value.Name - - @property - def value(self) -> int: - """Return the integer value of the enumeration value.""" - return self._value.Value - - @overload - @staticmethod - def wrap_enumeration_value( - value: dotnet.AF.Asset.AFEnumerationValue, - ) -> "AFEnumerationValue": ... - @overload - @staticmethod - def wrap_enumeration_value( - value: T, - ) -> T: ... - @staticmethod - def wrap_enumeration_value( - value: T | dotnet.AF.Asset.AFEnumerationValue, - ) -> "T | AFEnumerationValue": - """Wrap the value in an AFEnumerationValue if it is an enumeration value.""" - if isinstance(value, dotnet.lib.AF.Asset.AFEnumerationValue): - return AFEnumerationValue(value) - return value - - class AFAttribute(Data.DataContainer): """Representation of an AF attribute.""" @@ -158,7 +111,7 @@ def _normalize_filter_expression(self, filter_expression: str) -> str: def _current_value(self) -> object: """Return the current value of the attribute.""" - return AFEnumerationValue.wrap_enumeration_value(self.attribute.GetValue().Value) + return self.attribute.GetValue().Value def _filtered_summaries( self, diff --git a/PIconnect/Data.py b/PIconnect/Data.py index 8aef3eba3..422ef65ee 100644 --- a/PIconnect/Data.py +++ b/PIconnect/Data.py @@ -4,7 +4,7 @@ import datetime import enum from collections.abc import Callable -from typing import Any, Concatenate, Literal, ParamSpec, TypeVar, cast +from typing import Any, Concatenate, Literal, ParamSpec, TypeVar, cast, overload import pandas as pd # type: ignore @@ -30,7 +30,7 @@ class BoundaryType(enum.IntEnum): class SummaryType(enum.IntFlag): """SummaryType indicates which types of summary should be calculated. - `SummaryType`'s are `enum.IntFlag`'s and can be or'ed together to select + `SummaryType`'s are :class:`enum.IntFlag`'s and can be or'ed together to select multiple summary types. For example: >>> SummaryType.MINIMUM | SummaryType.MAXIMUM # Returns minimum and maximum @@ -69,7 +69,7 @@ class SummaryType(enum.IntFlag): TOTAL_WITH_UOM = 16384 #: A convenience to retrieve all summary types ALL = 24831 - #: A convenience to retrieve all summary types for non-numeric data + #: A convenience to retrieve all summary types available for non-numeric data ALL_FOR_NON_NUMERIC = 8320 @@ -208,6 +208,57 @@ class BufferMode(enum.IntEnum): _DEFAULT_TIMESTAMP_CALCULATION = TimestampCalculation.AUTO +T = TypeVar("T") + + +class AFEnumerationValue: + """Representation of an AF enumeration value.""" + + def __init__(self, value: dotnet.AF.Asset.AFEnumerationValue) -> None: + self._value = value + + def __str__(self) -> str: + """Return the string representation of the enumeration value.""" + return self._value.Name + + def __int__(self) -> int: + """Return the integer representation of the enumeration value.""" + return self._value.Value + + def __repr__(self): + """Return the string representation of the enumeration value.""" + return f"{self.__class__.__qualname__}({self._value.Name})" + + @property + def name(self) -> str: + """Return the name of the enumeration value.""" + return self._value.Name + + @property + def value(self) -> int: + """Return the integer value of the enumeration value.""" + return self._value.Value + + @overload + @staticmethod + def wrap_enumeration_value( + value: dotnet.AF.Asset.AFEnumerationValue, + ) -> "AFEnumerationValue": ... + @overload + @staticmethod + def wrap_enumeration_value( + value: T, + ) -> T: ... + @staticmethod + def wrap_enumeration_value( + value: T | dotnet.AF.Asset.AFEnumerationValue, + ) -> "T | AFEnumerationValue": + """Wrap the value in an AFEnumerationValue if it is an enumeration value.""" + if isinstance(value, dotnet.lib.AF.Asset.AFEnumerationValue): + return AFEnumerationValue(value) + return value + + class DataContainer(abc.ABC): """Abstract base class for data containers.""" @@ -226,7 +277,7 @@ def stepped_data(self) -> bool: @property def current_value(self) -> Any: """Return the current value of the attribute.""" - return self._current_value() + return AFEnumerationValue.wrap_enumeration_value(self._current_value()) @abc.abstractmethod def _current_value(self) -> Any: @@ -249,43 +300,41 @@ def filtered_summaries( Parameters ---------- - start_time (str or datetime): String containing the date, and possibly time, - from which to retrieve the values. This is parsed, together - with `end_time`, using - :afsdk:`AF.Time.AFTimeRange `. - end_time (str or datetime): String containing the date, and possibly time, - until which to retrieve values. This is parsed, together - with `start_time`, using - :afsdk:`AF.Time.AFTimeRange `. - interval (str, datetime.timedelta or pandas.Timedelta): String containing the - interval at which to extract data. This is parsed using - :afsdk:`AF.Time.AFTimeSpan.Parse `. - filter_expression (str, optional): Defaults to ''. Query on which - data to include in the results. See :ref:`filtering_values` - for more information on filter queries. - summary_types (int or PIConsts.SummaryType): Type(s) of summaries - of the data within the requested time range. - calculation_basis (int or PIConsts.CalculationBasis, optional): - Event weighting within an interval. See :ref:`event_weighting` - and :any:`CalculationBasis` for more information. Defaults to - CalculationBasis.TIME_WEIGHTED. - filter_evaluation (int or PIConsts.ExpressionSampleType, optional): - Determines whether the filter is applied to the raw events in - the database, of if it is applied to an interpolated series - with a regular interval. Defaults to - ExpressionSampleType.EXPRESSION_RECORDED_VALUES. - filter_interval (str, optional): String containing the interval at - which to extract apply the filter. This is parsed using - :afsdk:`AF.Time.AFTimeSpan.Parse `. - time_type (int or PIConsts.TimestampCalculation, optional): - Timestamp to return for each of the requested summaries. See - :ref:`summary_timestamps` and :any:`TimestampCalculation` for - more information. Defaults to TimestampCalculation.AUTO. + start_time : str or datetime + String containing the date, and possibly time, from which to retrieve the values. + This is parsed, together with `end_time`, using :func:`.Time.to_af_time_range`. + end_time : str or datetime + String containing the date, and possibly time, until which to retrieve values. This + is parsed, together with `start_time`, using :func:`.Time.to_af_time_range`. + interval : str, datetime.timedelta or pandas.Timedelta + String containing the interval at which to extract data. This is parsed using + :func:`.Time.to_af_time_span`. + filter_expression : str, optional + Defaults to ''. Query on which data to include in the results. See + :ref:`filtering_values` for more information on filter queries. + summary_types : int or Data.SummaryType + Type(s) of summaries of the data within the requested time range. + calculation_basis : int or Data.CalculationBasis, optional + Event weighting within an interval. See :ref:`event_weighting` and + :class:`.CalculationBasis` for more information. Defaults to + :attr:`.CalculationBasis.TIME_WEIGHTED`. + filter_evaluation : int or Data.ExpressionSampleType, optional + Determines whether the filter is applied to the raw events in the database, of if + it is applied to an interpolated series with a regular interval. Defaults to + :attr:`.ExpressionSampleType.EXPRESSION_RECORDED_VALUES`. + filter_interval : str, optional + String containing the interval at which to extract apply the filter. This is parsed + using :func:`.Time.to_af_time_span`. + time_type : int or Data.TimestampCalculation, optional + Timestamp to return for each of the requested summaries. See + :ref:`summary_timestamps` and :class:`.TimestampCalculation` for + more information. Defaults to :attr:`.TimestampCalculation.AUTO`. Returns ------- - pandas.DataFrame: Dataframe with the unique timestamps as row index - and the summary name as column name. + pandas.DataFrame + Dataframe with the unique timestamps as row index and the summary name as column + name. """ time_range = Time.to_af_time_range(start_time, end_time) _interval = Time.to_af_time_span(interval) @@ -310,7 +359,10 @@ def filtered_summaries( key = SummaryType(int(summary.Key)).name timestamps, values = zip( *[ - (Time.timestamp_to_index(value.Timestamp.UtcTime), value.Value) + ( + Time.timestamp_to_index(value.Timestamp.UtcTime), + AFEnumerationValue.wrap_enumeration_value(value.Value), + ) for value in summary.Value ], strict=True, @@ -340,19 +392,19 @@ def interpolated_value(self, time: Time.TimeLike) -> pd.Series: Parameters ---------- - time (str, datetime): String containing the date, and possibly time, - for which to retrieve the value. This is parsed, using - :ref:`Time.to_af_time`. + time : str, datetime + String containing the date, and possibly time, for which to retrieve the value. + This is parsed, using :func:`.Time.to_af_time`. Returns ------- - pd.Series: A pd.Series with a single row, with the corresponding time as - the index + pandas.Series + A pd.Series with a single row, with the corresponding time as the index """ _time = Time.to_af_time(time) pivalue = self._interpolated_value(_time) result = pd.Series( - data=[pivalue.Value], + data=[AFEnumerationValue.wrap_enumeration_value(pivalue.Value)], index=[Time.timestamp_to_index(pivalue.Timestamp.UtcTime)], name=self.name, ) @@ -372,13 +424,12 @@ def interpolated_values( ) -> pd.Series: """Return a pd.Series of interpolated data. - Data is returned between *start_time* and *end_time* at a fixed - *interval*. All three values are parsed by AF.Time and the first two - allow for time specification relative to "now" by use of the - asterisk. + Data is returned between *start_time* and *end_time* at a fixed *interval*. The first + two allow for time specification relative to "now" by use of the asterisk. - *filter_expression* is an optional string to filter the returned - values, see OSIsoft PI documentation for more information. + *filter_expression* is an optional string to filter the returned values, see the + `Performance equation `_ + documentation for more information. The AF SDK allows for inclusion of filtered data, with filtered values marked as such. At this point PIconnect does not support this @@ -386,21 +437,24 @@ def interpolated_values( Parameters ---------- - start_time (str or datetime): Containing the date, and possibly time, - from which to retrieve the values. This is parsed, together - with `end_time`, using :ref:`Time.to_af_time_range`. - end_time (str or datetime): Containing the date, and possibly time, - until which to retrieve values. This is parsed, together - with `start_time`, using :ref:`Time.to_af_time_range`. - interval (str, datetime.timedelta or pd.Timedelta): String containing the interval - at which to extract data. This is parsed using :ref:`Time.to_af_time_span`. - filter_expression (str, optional): Defaults to ''. Query on which - data to include in the results. See :ref:`filtering_values` - for more information on filter queries. + start_time : str or datetime.datetime + Containing the date, and possibly time, from which to retrieve the values. This is + parsed, together with `end_time`, using :func:`.Time.to_af_time_range`. + end_time : str or datetime.datetime + Containing the date, and possibly time, until which to retrieve values. This is + parsed, together with `start_time`, using + :func:`.Time.to_af_time_range`. + interval : str, datetime.timedelta or pandas.Timedelta + String containing the interval at which to extract data. This is parsed using + :func:`.Time.to_af_time_span`. + filter_expression : str, optional + Defaults to ''. Query on which data to include in the results. See + :ref:`filtering_values` for more information on filter queries. Returns ------- - pd.Series: Timeseries of the values returned by the SDK + pandas.Series + Timeseries of the values returned by the SDK """ time_range = Time.to_af_time_range(start_time, end_time) _interval = Time.to_af_time_span(interval) @@ -411,7 +465,7 @@ def interpolated_values( values: list[Any] = [] for value in pivalues: timestamps.append(Time.timestamp_to_index(value.Timestamp.UtcTime)) - values.append(value.Value) + values.append(AFEnumerationValue.wrap_enumeration_value(value.Value)) result = pd.Series( data=values, index=timestamps, @@ -441,23 +495,23 @@ def recorded_value( Parameters ---------- - time (str): String containing the date, and possibly time, - for which to retrieve the value. This is parsed, using - :afsdk:`AF.Time.AFTime `. - retrieval_mode (int or :any:`PIConsts.RetrievalMode`): Flag determining - which value to return if no value available at the exact requested - time. + time : str + String containing the date, and possibly time, for which to retrieve the value. + This is parsed, using :func:`.Time.to_af_time`. + retrieval_mode : int or RetrievalMode + Flag determining which value to return if no value available at the exact requested + time. Returns ------- - pd.Series: A pd.Series with a single row, with the corresponding time as - the index + pandas.Series + A pd.Series with a single row, with the corresponding time as the index. """ _time = Time.to_af_time(time) _retrieval_mode = dotnet.lib.AF.Data.AFRetrievalMode(int(retrieval_mode)) pivalue = self._recorded_value(_time, _retrieval_mode) result = pd.Series( - data=[pivalue.Value], + data=[AFEnumerationValue.wrap_enumeration_value(pivalue.Value)], index=[Time.timestamp_to_index(pivalue.Timestamp.UtcTime)], name=self.name, ) @@ -500,21 +554,24 @@ def recorded_values( Parameters ---------- - start_time (str or datetime): Containing the date, and possibly time, - from which to retrieve the values. This is parsed, together - with `end_time`, using :ref:`Time.to_af_time_range`. - end_time (str or datetime): Containing the date, and possibly time, - until which to retrieve values. This is parsed, together - with `start_time`, using :ref:`Time.to_af_time_range`. - boundary_type (BoundaryType): Specification for how to handle values near the - specified start and end time. Defaults to `BoundaryType.INSIDE`. - filter_expression (str, optional): Defaults to ''. Query on which - data to include in the results. See :ref:`filtering_values` - for more information on filter queries. + start_time : str or datetime + Containing the date, and possibly time, from which to retrieve the values. This is + parsed, together with `end_time`, using :func:`.Time.to_af_time_range`. + end_time : str or datetime + Containing the date, and possibly time, until which to retrieve values. This is + parsed, together with `start_time`, using :func:`.Time.to_af_time_range`. + boundary_type : BoundaryType + Specification for how to handle values near the specified start and end time. + Defaults to :attr:`.BoundaryType.INSIDE`. + filter_expression : str, optional + Defaults to ''. Query on which + data to include in the results. See :ref:`filtering_values` + for more information on filter queries. Returns ------- - pd.Series: Timeseries of the values returned by the SDK + pandas.Series + Timeseries of the values returned by the SDK """ time_range = Time.to_af_time_range(start_time, end_time) _boundary_type = dotnet.lib.AF.Data.AFBoundaryType(int(boundary_type)) @@ -526,7 +583,7 @@ def recorded_values( values: list[Any] = [] for value in pivalues: timestamps.append(Time.timestamp_to_index(value.Timestamp.UtcTime)) - values.append(value.Value) + values.append(AFEnumerationValue.wrap_enumeration_value(value.Value)) result = pd.Series( data=values, index=timestamps, @@ -562,27 +619,28 @@ def summary( Parameters ---------- - start_time (str or datetime): Containing the date, and possibly time, - from which to retrieve the values. This is parsed, together - with `end_time`, using :ref:`Time.to_af_time_range`. - end_time (str or datetime): Containing the date, and possibly time, - until which to retrieve values. This is parsed, together - with `start_time`, using :ref:`Time.to_af_time_range`. - summary_types (int or SummaryType): Type(s) of summaries - of the data within the requested time range. - calculation_basis (int or CalculationBasis, optional): - Event weighting within an interval. See :ref:`event_weighting` - and :any:`CalculationBasis` for more information. Defaults to - CalculationBasis.TIME_WEIGHTED. - time_type (int or TimestampCalculation, optional): - Timestamp to return for each of the requested summaries. See - :ref:`summary_timestamps` and :any:`TimestampCalculation` for - more information. Defaults to TimestampCalculation.AUTO. + start_time : str or datetime + Containing the date, and possibly time, from which to retrieve the values. This is + parsed, together with `end_time`, using :func:`.Time.to_af_time_range`. + end_time : str or datetime + Containing the date, and possibly time, until which to retrieve values. This is + parsed, together with `start_time`, using :func:`.Time.to_af_time_range`. + summary_types : int or SummaryType + Type(s) of summaries of the data within the requested time range. + calculation_basis : int or CalculationBasis, optional + Event weighting within an interval. See :ref:`event_weighting` and + :class:`.CalculationBasis` for more information. Defaults to + :attr:`.CalculationBasis.TIME_WEIGHTED`. + time_type : int or TimestampCalculation, optional + Timestamp to return for each of the requested summaries. See + :ref:`summary_timestamps` and :class:`.TimestampCalculation` for + more information. Defaults to :attr:`.TimestampCalculation.AUTO`. Returns ------- - pandas.DataFrame: Dataframe with the unique timestamps as row index - and the summary name as column name. + pandas.DataFrame + Dataframe with the unique timestamps as row index and the summary name as column + name. """ time_range = Time.to_af_time_range(start_time, end_time) _summary_types = dotnet.lib.AF.Data.AFSummaryTypes(int(summary_types)) @@ -592,7 +650,7 @@ def summary( df = pd.DataFrame() for summary in pivalues: key = SummaryType(int(summary.Key)).name - value = summary.Value + value = AFEnumerationValue.wrap_enumeration_value(summary.Value) timestamp = Time.timestamp_to_index(value.Timestamp.UtcTime) value = value.Value df = df.join( @@ -624,29 +682,31 @@ def summaries( Parameters ---------- - start_time (str or datetime): Containing the date, and possibly time, - from which to retrieve the values. This is parsed, together - with `end_time`, using :ref:`Time.to_af_time_range`. - end_time (str or datetime): Containing the date, and possibly time, - until which to retrieve values. This is parsed, together - with `start_time`, using :ref:`Time.to_af_time_range`. - interval (str, datetime.timedelta or pd.Timedelta): String containing the interval - at which to extract data. This is parsed using :ref:`Time.to_af_time_span`. - summary_types (int or PIConsts.SummaryType): Type(s) of summaries - of the data within the requested time range. - calculation_basis (int or PIConsts.CalculationBasis, optional): - Event weighting within an interval. See :ref:`event_weighting` - and :any:`CalculationBasis` for more information. Defaults to - CalculationBasis.TIME_WEIGHTED. - time_type (int or PIConsts.TimestampCalculation, optional): - Timestamp to return for each of the requested summaries. See - :ref:`summary_timestamps` and :any:`TimestampCalculation` for - more information. Defaults to TimestampCalculation.AUTO. + start_time : str or datetime + Containing the date, and possibly time, from which to retrieve the values. This is + parsed, together with `end_time`, using :func:`.Time.to_af_time_range`. + end_time : str or datetime + Containing the date, and possibly time, until which to retrieve values. This is + parsed, together with `start_time`, using :func:`.Time.to_af_time_range`. + interval : str, datetime.timedelta or pandas.Timedelta + String containing the interval at which to extract data. This is parsed using + :func:`.Time.to_af_time_span`. + summary_types : int or SummaryType + Type(s) of summaries of the data within the requested time range. + calculation_basis : int or CalculationBasis, optional + Event weighting within an interval. See :ref:`event_weighting` and + :class:`.CalculationBasis` for more information. Defaults to + :attr:`.CalculationBasis.TIME_WEIGHTED`. + time_type : int or TimestampCalculation, optional + Timestamp to return for each of the requested summaries. See + :ref:`summary_timestamps` and :class:`.TimestampCalculation` for more + information. Defaults to :attr:`.TimestampCalculation.AUTO`. Returns ------- - pandas.DataFrame: Dataframe with the unique timestamps as row index - and the summary name as column name. + pandas.DataFrame + Dataframe with the unique timestamps as row index and the summary name as column + name. """ time_range = Time.to_af_time_range(start_time, end_time) _interval = Time.to_af_time_span(interval) @@ -661,7 +721,10 @@ def summaries( key = SummaryType(int(summary.Key)).name timestamps, values = zip( *[ - (Time.timestamp_to_index(value.Timestamp.UtcTime), value.Value) + ( + Time.timestamp_to_index(value.Timestamp.UtcTime), + AFEnumerationValue.wrap_enumeration_value(value.Value), + ) for value in summary.Value ], strict=True, @@ -700,10 +763,12 @@ def update_value( Parameters ---------- - value: value type should be in cohesion with PI object or - it will raise PIException: [-10702] STATE Not Found - time (datetime, optional): it is not possible to set future value, - it raises PIException: [-11046] Target Date in Future. + value: + value type should be in cohesion with PI object or it will raise + `PIException: [-10702] STATE Not Found`. + time : datetime, optional + It is not possible to set future value, it raises + `PIException: [-11046] Target Date in Future`. You can combine update_mode and time to change already stored value. """ @@ -769,6 +834,23 @@ def apply_func(element: DataContainerType) -> pd.DataFrame: df = result.to_frame() return add_name_to_index(df, element) + def add_rank_to_index(df: pd.DataFrame) -> pd.DataFrame: + rank: "pd.Series[int]" = ( # type: ignore + df.index.to_series().groupby(level=0).cumcount().rename("__rank__") + 1 # type: ignore + ) + return df.set_index(rank, append=True) # type: ignore + + def concat_dfs(dfs: list[pd.DataFrame]) -> pd.DataFrame: + match len(dfs): + case 0: + return pd.DataFrame() + case 1: + return dfs[0] + case _: + return pd.concat( + [add_rank_to_index(df) for df in dfs], axis=1 + ).reset_index(level="__rank__", drop=True) + def align(df: pd.DataFrame) -> pd.DataFrame: match _align: case False: @@ -793,12 +875,7 @@ def align(df: pd.DataFrame) -> pd.DataFrame: case "time": return df.interpolate(method="time", axis=0) # type: ignore - return align( - pd.concat( - [pd.DataFrame()] + [apply_func(e) for e in self._elements], - axis=1, - ) - ) + return align(concat_dfs([apply_func(e) for e in self._elements])) @property def current_value(self) -> pd.Series: @@ -855,14 +932,14 @@ def interpolated_value(self, time: Time.TimeLike, align: Align = False) -> pd.Da Parameters ---------- - time (str, datetime): String containing the date, and possibly time, - for which to retrieve the value. This is parsed, using - :ref:`Time.to_af_time`. + time : str, datetime + String containing the date, and possibly time, for which to retrieve the value. + This is parsed, using :func:`.Time.to_af_time`. Returns ------- - pd.Series: A pd.Series with a single row, with the corresponding time as - the index + pd.Series + A pd.Series with a single row, with the corresponding time as the index """ return self._combine_dfs_to_df( self._element_type.interpolated_value, _align=align, time=time @@ -878,17 +955,15 @@ def interpolated_values( ) -> pd.DataFrame: """Return a pd.DataFrame of interpolated data. - Data is returned between *start_time* and *end_time* at a fixed - *interval*. All three values are parsed by AF.Time and the first two - allow for time specification relative to "now" by use of the - asterisk. + Data is returned between *start_time* and *end_time* at a fixed *interval*. The first + two allow for time specification relative to "now" by use of the asterisk. - *filter_expression* is an optional string to filter the returned - values, see OSIsoft PI documentation for more information. + *filter_expression* is an optional string to filter the returned values, see OSIsoft PI + documentation for more information. - The AF SDK allows for inclusion of filtered data, with filtered - values marked as such. At this point PIconnect does not support this - and filtered values are always left out entirely. + The AF SDK allows for inclusion of filtered data, with filtered values marked as such. + At this point PIconnect does not support this and filtered values are always left out + entirely. .. warning:: Relative times are evaluated for each element in the collection, @@ -901,21 +976,23 @@ def interpolated_values( Parameters ---------- - start_time (str or datetime): Containing the date, and possibly time, - from which to retrieve the values. This is parsed, together - with `end_time`, using :ref:`Time.to_af_time_range`. - end_time (str or datetime): Containing the date, and possibly time, - until which to retrieve values. This is parsed, together - with `start_time`, using :ref:`Time.to_af_time_range`. - interval (str, datetime.timedelta or pd.Timedelta): String containing the interval - at which to extract data. This is parsed using :ref:`Time.to_af_time_span`. - filter_expression (str, optional): Defaults to ''. Query on which - data to include in the results. See :ref:`filtering_values` - for more information on filter queries. + start_time : str or datetime + Containing the date, and possibly time, from which to retrieve the values. This is + parsed, together with `end_time`, using :func:`.Time.to_af_time_range`. + end_time : str or datetime + Containing the date, and possibly time, until which to retrieve values. This is + parsed, together with `start_time`, using :func:`.Time.to_af_time_range`. + interval : str, datetime.timedelta or pd.Timedelta + String containing the interval at which to extract data. This is parsed using + :func:`.Time.to_af_time_span`. + filter_expression : str, optional + Defaults to ''. Query on which data to include in the results. See + :ref:`filtering_values` for more information on filter queries. Returns ------- - pd.DataFrame: Timeseries of the values returned by the SDK + pd.DataFrame + Timeseries of the values returned by the SDK """ return self._combine_dfs_to_df( self._element_type.interpolated_values, @@ -936,17 +1013,17 @@ def recorded_value( Parameters ---------- - time (str): String containing the date, and possibly time, - for which to retrieve the value. This is parsed, using - :afsdk:`AF.Time.AFTime `. - retrieval_mode (int or :any:`PIConsts.RetrievalMode`): Flag determining - which value to return if no value available at the exact requested - time. + time : str + String containing the date, and possibly time, for which to retrieve the value. + This is parsed, using :func:`.Time.to_af_time`. + retrieval_mode : int or RetrievalMode + Flag determining which value to return if no value available at the exact requested + time. Returns ------- - pd.Series: A pd.Series with a single row, with the corresponding time as - the index + pd.Series + A pd.Series with a single row, with the corresponding time as the index. """ return self._combine_dfs_to_df( self._element_type.recorded_value, @@ -965,20 +1042,9 @@ def recorded_values( ) -> pd.DataFrame: """Return a pd.Series of recorded data. - Data is returned between the given *start_time* and *end_time*, - inclusion of the boundaries is determined by the *boundary_type* - attribute. Both *start_time* and *end_time* are parsed by AF.Time and - allow for time specification relative to "now" by use of the asterisk. - - By default the *boundary_type* is set to 'inside', which returns from - the first value after *start_time* to the last value before *end_time*. - The other options are 'outside', which returns from the last value - before *start_time* to the first value before *end_time*, and - 'interpolate', which interpolates the first value to the given - *start_time* and the last value to the given *end_time*. - - *filter_expression* is an optional string to filter the returned - values, see OSIsoft PI documentation for more information. + Data is returned between the given *start_time* and *end_time*, inclusion of the + boundaries is determined by the *boundary_type* attribute. Both *start_time* and + *end_time* and allow for time specification relative to "now" by use of the asterisk. The AF SDK allows for inclusion of filtered data, with filtered values marked as such. At this point PIconnect does not support this and @@ -986,21 +1052,23 @@ def recorded_values( Parameters ---------- - start_time (str or datetime): Containing the date, and possibly time, - from which to retrieve the values. This is parsed, together - with `end_time`, using :ref:`Time.to_af_time_range`. - end_time (str or datetime): Containing the date, and possibly time, - until which to retrieve values. This is parsed, together - with `start_time`, using :ref:`Time.to_af_time_range`. - boundary_type (BoundaryType): Specification for how to handle values near the - specified start and end time. Defaults to `BoundaryType.INSIDE`. - filter_expression (str, optional): Defaults to ''. Query on which - data to include in the results. See :ref:`filtering_values` - for more information on filter queries. + start_time : str or datetime + Containing the date, and possibly time, from which to retrieve the values. This is + parsed, together with `end_time`, using :func:`.Time.to_af_time_range`. + end_time : str or datetime + Containing the date, and possibly time, until which to retrieve values. This is + parsed, together with `start_time`, using :func:`.Time.to_af_time_range`. + boundary_type : BoundaryType + Specification for how to handle values near the specified start and end time. + Defaults to :attr:`.BoundaryType.INSIDE`. + filter_expression : str, optional + Defaults to ''. Query on which data to include in the results. See + :ref:`filtering_values` for more information on filter queries. Returns ------- - pd.Series: Timeseries of the values returned by the SDK + pd.Series + Timeseries of the values returned by the SDK """ return self._combine_dfs_to_df( self._element_type.recorded_values, @@ -1024,27 +1092,28 @@ def summary( Parameters ---------- - start_time (str or datetime): Containing the date, and possibly time, - from which to retrieve the values. This is parsed, together - with `end_time`, using :ref:`Time.to_af_time_range`. - end_time (str or datetime): Containing the date, and possibly time, - until which to retrieve values. This is parsed, together - with `start_time`, using :ref:`Time.to_af_time_range`. - summary_types (int or SummaryType): Type(s) of summaries - of the data within the requested time range. - calculation_basis (int or CalculationBasis, optional): - Event weighting within an interval. See :ref:`event_weighting` - and :any:`CalculationBasis` for more information. Defaults to - CalculationBasis.TIME_WEIGHTED. - time_type (int or TimestampCalculation, optional): - Timestamp to return for each of the requested summaries. See - :ref:`summary_timestamps` and :any:`TimestampCalculation` for - more information. Defaults to TimestampCalculation.AUTO. + start_time : str or datetime + Containing the date, and possibly time, from which to retrieve the values. This is + parsed, together with `end_time`, using :func:`.Time.to_af_time_range`. + end_time : str or datetime + Containing the date, and possibly time, until which to retrieve values. This is + parsed, together with `start_time`, using :func:`.Time.to_af_time_range`. + summary_types : int or SummaryType + Type(s) of summaries of the data within the requested time range. + calculation_basis : int or CalculationBasis, optional + Event weighting within an interval. See :ref:`event_weighting` and + :class:`.CalculationBasis` for more information. Defaults to + :attr:`.CalculationBasis.TIME_WEIGHTED`. + time_type : int or TimestampCalculation, optional + Timestamp to return for each of the requested summaries. See + :ref:`summary_timestamps` and :class:`.TimestampCalculation` for more information. + Defaults to :attr:`.TimestampCalculation.AUTO`. Returns ------- - pandas.DataFrame: Dataframe with the unique timestamps as row index - and the summary name as column name. + pandas.DataFrame + Dataframe with the unique timestamps as row index and the summary name as column + name. """ return self._combine_dfs_to_df( self._element_type.summary, @@ -1071,29 +1140,31 @@ def summaries( Parameters ---------- - start_time (str or datetime): Containing the date, and possibly time, - from which to retrieve the values. This is parsed, together - with `end_time`, using :ref:`Time.to_af_time_range`. - end_time (str or datetime): Containing the date, and possibly time, - until which to retrieve values. This is parsed, together - with `start_time`, using :ref:`Time.to_af_time_range`. - interval (str, datetime.timedelta or pd.Timedelta): String containing the interval - at which to extract data. This is parsed using :ref:`Time.to_af_time_span`. - summary_types (int or PIConsts.SummaryType): Type(s) of summaries - of the data within the requested time range. - calculation_basis (int or PIConsts.CalculationBasis, optional): - Event weighting within an interval. See :ref:`event_weighting` - and :any:`CalculationBasis` for more information. Defaults to - CalculationBasis.TIME_WEIGHTED. - time_type (int or PIConsts.TimestampCalculation, optional): - Timestamp to return for each of the requested summaries. See - :ref:`summary_timestamps` and :any:`TimestampCalculation` for - more information. Defaults to TimestampCalculation.AUTO. + start_time : str or datetime + Containing the date, and possibly time, from which to retrieve the values. This is + parsed, together with `end_time`, using :func:`.Time.to_af_time_range`. + end_time : str or datetime + Containing the date, and possibly time, until which to retrieve values. This is + parsed, together with `start_time`, using :func:`.Time.to_af_time_range`. + interval : str, datetime.timedelta or pd.Timedelta + String containing the interval at which to extract data. This is parsed using + :func:`.Time.to_af_time_span`. + summary_types : int or SummaryType + Type(s) of summaries of the data within the requested time range. + calculation_basis : int or CalculationBasis, optional + Event weighting within an interval. See :ref:`event_weighting` and + :class:`.CalculationBasis` for more information. Defaults to + :attr:`.CalculationBasis.TIME_WEIGHTED`. + time_type : int or TimestampCalculation, optional + Timestamp to return for each of the requested summaries. See + :ref:`summary_timestamps` and :class:`.TimestampCalculation` for more information. + Defaults to :attr:`.TimestampCalculation.AUTO`. Returns ------- - pandas.DataFrame: Dataframe with the unique timestamps as row index - and the summary name as column name. + pandas.DataFrame + Dataframe with the unique timestamps as row index and the summary name as column + name. """ return self._combine_dfs_to_df( self._element_type.summaries, diff --git a/PIconnect/PI.py b/PIconnect/PI.py index f1e86f9dd..7648ad18b 100644 --- a/PIconnect/PI.py +++ b/PIconnect/PI.py @@ -64,7 +64,8 @@ class PIPoint(Data.DataContainer): Parameters ---------- - pi_point (AF.PI.PIPoint): Reference to a PIPoint as returned by the SDK + pi_point : :afsdk:`AF.PI.PIPoint ` + Reference to a PIPoint as returned by the SDK """ version = "0.3.0" @@ -224,16 +225,24 @@ def _update_value( return self.pi_point.UpdateValue(value, update_mode, buffer_mode) -class PIServer(object): # pylint: disable=useless-object-inheritance +class PIServer: """PIServer is a connection to an OSIsoft PI Server. Parameters ---------- - server (str, optional): Name of the server to connect to, defaults to None - username (str, optional): can be used only with password as well - password (str, optional): -//- - todo: domain, auth - timeout (int, optional): the maximum seconds an operation can take + server : str, optional + Name of the server to connect to, defaults to None + username : str, optional + Username to connect to the server, defaults to None + password : str, optional + Password for the username, defaults to None + domain : str, optional + Domain of the username, defaults to None + authentication_mode : AuthenticationMode, optional + Authentication mode to use, defaults to PI_USER_AUTHENTICATION + timeout : int, optional + the maximum seconds an operation can take + .. note:: If the specified `server` is unknown a warning is thrown and the connection @@ -241,9 +250,6 @@ class PIServer(object): # pylint: disable=useless-object-inheritance of known servers is available in the `PIServer.servers` dictionary. """ - version = "0.2.2" - - #: Dictionary of known servers, as reported by the SDK _servers: dict[str, dotnet.AF.PI.PIServer] | None = None _default_server: dotnet.AF.PI.PIServer | None = None @@ -339,30 +345,37 @@ def server_name(self): """Name of the connected server.""" return self.connection.Name - def search(self, query: str | list[str], source: str | None = None) -> list[PIPoint]: + def search( + self, query: str | list[str], source: str | None = None + ) -> Data.DataContainerCollection[PIPoint]: """Search PIPoints on the PIServer. Parameters ---------- - query (str or [str]): String or list of strings with queries - source (str, optional): Defaults to None. Point source to limit the results + query : str or [str] + String or list of strings with queries + source : str, optional + Defaults to None. Point source to limit the results Returns ------- - list: A list of :class:`PIPoint` objects as a result of the query + Data.DataContainerCollection[PIPoint] + A collection of :class:`PIPoint` objects as a result of the query. + .. todo:: Reject searches while not connected """ if isinstance(query, list): - return [y for x in query for y in self.search(x, source)] - # elif not isinstance(query, str): - # raise TypeError('Argument query must be either a string or a list of strings,' + - # 'got type ' + str(type(query))) - return [ - PIPoint(pi_point) - for pi_point in dotnet.lib.AF.PI.PIPoint.FindPIPoints( - self.connection, str(query), source, None + return Data.DataContainerCollection( + [y for x in query for y in self.search(x, source)] ) - ] + return Data.DataContainerCollection( + [ + PIPoint(pi_point) + for pi_point in dotnet.lib.AF.PI.PIPoint.FindPIPoints( + self.connection, str(query), source, None + ) + ] + ) diff --git a/PIconnect/PIConsts.py b/PIconnect/PIConsts.py index 7fd645702..3a288e2c0 100644 --- a/PIconnect/PIConsts.py +++ b/PIconnect/PIConsts.py @@ -1,215 +1,27 @@ -"""Enumerations for PI options.""" - -import enum - - -class UpdateMode(enum.IntEnum): - """Indicates how to treat duplicate values in the archive. - - Only used when supported by the Data Reference. - - Detailed information is available at - :afsdk:`AF.Data.AFUpdateOption ` - """ - - #: Add the value to the archive. - #: If any values exist at the same time, will overwrite one of them and set its - #: Substituted flag. - REPLACE = 0 - #: Add the value to the archive. Any existing values at the same time are not overwritten. - INSERT = 1 - #: Add the value to the archive only if no value exists at the same time. - #: If a value already exists for that time, the passed value is ignored. - NO_REPLACE = 2 - #: Replace an existing value in the archive at the specified time. - #: If no existing value is found, the passed value is ignored. - REPLACE_ONLY = 3 - #: Add the value to the archive without compression. - #: If this value is written to the snapshot, the previous snapshot value will be written to - #: the archive, - #: without regard to compression settings. - #: Note that if a subsequent snapshot value is written without the InsertNoCompression - #: option, - #: the value added with the InsertNoCompression option is still subject to compression. - INSERT_NO_COMPRESSION = 5 - #: Remove the value from the archive if a value exists at the passed time. - REMOVE = 6 - - -class BufferMode(enum.IntEnum): - """Indicates buffering option in updating values, when supported by the Data Reference. - - Detailed information is available at - :afsdk:`AF.Data.AFBufferOption ` - """ - - #: Updating data reference values without buffer. - DO_NOT_BUFFER = 0 - #: Try updating data reference values with buffer. - #: If fails (e.g. data reference AFDataMethods does not support Buffering, - #: or its Buffering system is not available), - #: then try updating directly without buffer. - BUFFER_IF_POSSIBLE = 1 - # Updating data reference values with buffer. - BUFFER = 2 - - -class CalculationBasis(enum.IntEnum): - """CalculationBasis indicates how values should be weighted over a time range. - - Detailed information is available at - :afsdk:`AF.Data.AFCalculationBasis `. - """ - - #: Each event is weighted according to the time over which it applies. - TIME_WEIGHTED = 0 - #: Each event is weighted equally. - EVENT_WEIGHTED = 1 - #: Each event is time weighted, but interpolation is always done as if it is - #: continous data. - TIME_WEIGHTED_CONTINUOUS = 2 - #: Each event is time weighted, but interpolation is always done as if it is - #: discrete, stepped, data. - TIME_WEIGHTED_DISCRETE = 3 - #: Each event is weighted equally, except data at the end of the interval is - #: excluded. - EVENT_WEIGHTED_EXCLUDE_MOST_RECENT = 4 - #: Each event is weighted equally, except data at the beginning of the interval - #: is excluded. - EVENT_WEIGHTED_EXCLUDE_EARLIEST = 5 - #: Each event is weighted equally, data at both boundaries of the interval are - #: explicitly included. - EVENT_WEIGHTED_INCLUDE_BOTH_ENDS = 6 - - -class ExpressionSampleType(enum.IntEnum): - """ExpressionSampleType indicates how expressions are evaluated over a time range. - - Detailed information is available at - :afsdk:`AF.Data.AFSampleType `. - """ - - #: The expression is evaluated at each archive event. - EXPRESSION_RECORDED_VALUES = 0 - #: The expression is evaluated at a sampling interval, passed as a separate argument. - INTERVAL = 1 - - -class RetrievalMode(enum.IntEnum): - """RetrievalMode indicates which recorded value should be returned. - - Detailed information is available at - :afsdk:`AF.Data.AFRetrievalMode `. - """ - - #: Autmatic detection - AUTO = 0 - #: At the exact time if available, else the first before the requested time - AT_OR_BEFORE = 1 - #: The first before the requested time - BEFORE = 6 - #: At the exact time if available, else the first after the requested time - AT_OR_AFTER = 2 - #: The first after the requested time - AFTER = 7 - #: At the exact time if available, else return an error - EXACT = 4 - - -class SummaryType(enum.IntFlag): - """SummaryType indicates which types of summary should be calculated. - - Based on :class:`enum.IntEnum` in Python 3.5 or earlier. `SummaryType`'s can - be or'ed together. Python 3.6 or higher returns a new `IntFlag`, while in - previous versions it will be casted down to `int`. - - >>> SummaryType.MINIMUM | SummaryType.MAXIMUM # Returns minimum and maximum - # On Python 3.6+ - 12 # On previous versions - - Detailed information is available at - :afsdk:`AF.Data.AFSummaryTypes `. - """ - - #: No summary data - NONE = 0 - #: A total over the time span - TOTAL = 1 - #: Average value over the time span - AVERAGE = 2 - #: The minimum value in the time span - MINIMUM = 4 - #: The maximum value in the time span - MAXIMUM = 8 - #: The range of the values (max-min) in the time span - RANGE = 16 - #: The sample standard deviation of the values over the time span - STD_DEV = 32 - #: The population standard deviation of the values over the time span - POP_STD_DEV = 64 - #: The sum of the event count (when the calculation is event weighted). - #: The sum of the event time duration (when the calculation is time weighted.) - COUNT = 128 - #: The percentage of the data with a good value over the time range. - #: Based on time for time weighted calculations, - #: based on event count for event weigthed calculations. - PERCENT_GOOD = 8192 - #: The total over the time span, - #: with the unit of measurement that's associated with the input - #: (or no units if not defined for the input). - TOTAL_WITH_UOM = 16384 - #: A convenience to retrieve all summary types - ALL = 24831 - #: A convenience to retrieve all summary types for non-numeric data - ALL_FOR_NON_NUMERIC = 8320 - - -class TimestampCalculation(enum.IntEnum): - """ - TimestampCalculation defines the timestamp returned for a given summary calculation. - - Detailed information is available at - :afsdk:`AF.Data.AFTimeStampCalculation `. - """ - - #: The timestamp is the event time of the minimum or maximum for those summaries - #: or the beginning of the interval otherwise. - AUTO = 0 - #: The timestamp is always the beginning of the interval. - EARLIEST_TIME = 1 - #: The timestamp is always the end of the interval. - MOST_RECENT_TIME = 2 - - -class EventFrameSearchMode(enum.IntEnum): - """EventFrameSearchMode. - - EventFrameSearchMode defines the interpretation and direction from the start time - when searching for event frames. - - Detailed information is available at - :afsdk:`AF.EventFrame.AFEventFrameSearchMode `. - including a graphical display of event frames that are returned for a given search - mode. - """ # noqa: E501 - - #: Uninitialized - NONE = 0 - #: Backward from start time, also known as starting before - BACKWARD_FROM_START_TIME = 1 - STARTING_BEFORE = 1 - #: Forward from start time, also known as starting after - FORWARD_FROM_START_TIME = 2 - STARTING_AFTER = 2 - #: Backward from end time, also known as ending before - BACKWARD_FROM_END_TIME = 3 - ENDING_BEFORE = 3 - #: Forward from end time, also known as ending after - FORWARD_FROM_END_TIME = 4 - ENDING_AFTER = 4 - #: Backward in progress, also known as starting before and in progress - BACKWARD_IN_PROGRESS = 5 - STARTING_BEFORE_IN_PROGRESS = 5 - #: Forward in progress, also known as starting after and in progress - FORWARD_IN_PROGRESS = 6 - STARTING_AFTER_IN_PROGRESS = 6 +"""Central location for all PI enumerations.""" + +from .Data import ( + BoundaryType, + BufferMode, + CalculationBasis, + ExpressionSampleType, + RetrievalMode, + SummaryType, + TimestampCalculation, + UpdateMode, +) +from .EventFrame import EventFrameSearchMode +from .PI import AuthenticationMode + +__all__ = [ + "AuthenticationMode", + "BoundaryType", + "BufferMode", + "CalculationBasis", + "EventFrameSearchMode", + "ExpressionSampleType", + "RetrievalMode", + "SummaryType", + "TimestampCalculation", + "UpdateMode", +] diff --git a/PIconnect/Search.py b/PIconnect/Search.py index fd781cb78..44ea61275 100644 --- a/PIconnect/Search.py +++ b/PIconnect/Search.py @@ -7,12 +7,7 @@ from . import Asset, EventFrame, dotnet -SearchResultType = TypeVar( - "SearchResultType", - # PIAFBase.PIAFElement, - # PIAFBase.PIAFEventFrame, - # PIAFAttribute.PIAFAttribute, -) +SearchResultType = TypeVar("SearchResultType") AFSearchResultType = TypeVar("AFSearchResultType", covariant=True) @@ -44,9 +39,15 @@ def __iter__(self) -> Iterator[SearchResultType]: def one(self) -> SearchResultType: """Return the only item in the search result. + Returns + ------- + SearchResultType + The only item in the search result. + Raises ------ - ValueError: If there are no results or more than one result. + ValueError + If there are no results or more than one result. """ if self.count == 0: raise ValueError("No results found") @@ -70,8 +71,10 @@ def __init__( super().__init__(search) self.result_type = Asset.AFAttribute - def to_list(self) -> Asset.AFAttributeList: - """Return all items in the search result.""" + def one(self) -> Asset.AFAttribute: # noqa: D102 + return super().one() + + def to_list(self) -> Asset.AFAttributeList: # noqa: D102 return Asset.AFAttributeList(list(self)) @@ -85,8 +88,10 @@ def __init__( super().__init__(search) self.result_type = Asset.AFElement - def to_list(self) -> Asset.AFElementList: - """Return all items in the search result.""" + def one(self) -> Asset.AFElement: # noqa: D102 + return super().one() + + def to_list(self) -> Asset.AFElementList: # noqa: D102 return Asset.AFElementList(list(self)) @@ -102,8 +107,10 @@ def __init__( super().__init__(search) self.result_type = EventFrame.AFEventFrame - def to_list(self) -> EventFrame.AFEventFrameList: - """Return all items in the search result.""" + def one(self) -> EventFrame.AFEventFrame: # noqa: D102 + return super().one() + + def to_list(self) -> EventFrame.AFEventFrameList: # noqa: D102 return EventFrame.AFEventFrameList(list(self)) diff --git a/PIconnect/config.py b/PIconnect/config.py index a9e464fc4..e473e8758 100644 --- a/PIconnect/config.py +++ b/PIconnect/config.py @@ -26,4 +26,5 @@ def DEFAULT_TIMEZONE(self, value: str) -> None: self._default_timezone = value +#: Global configuration object for PIconnect package. PIConfig = PIConfigContainer() diff --git a/PIconnect/dotnet.py b/PIconnect/dotnet.py index 38abd48ac..32d2dd136 100644 --- a/PIconnect/dotnet.py +++ b/PIconnect/dotnet.py @@ -1,4 +1,4 @@ -"""AFSDK - Loads the .NET libraries from the OSIsoft AF SDK.""" +"""Loads the .NET libraries from the OSIsoft AF SDK.""" import logging import os @@ -43,7 +43,7 @@ def AF_SDK_VERSION(self) -> str: return self.AF.PISystems().Version def load(self, assembly_path: StrPath | None = None) -> None: - """Return a new instance of the PI connector.""" + """Load the AF SDK from the specified path.""" full_path = _get_SDK_path(assembly_path) if full_path is None: if assembly_path: @@ -57,6 +57,10 @@ def load(self, assembly_path: StrPath | None = None) -> None: logger.info("Loaded AF SDK version %s", self._af_sdk_version) def load_test_SDK(self) -> None: + """Load the test SDK. + + This is used for testing purposes only and should not be used in production. + """ self._af = AF self._system = System self._af_sdk_version = AF_SDK_VERSION @@ -95,7 +99,10 @@ def _get_SDK_path(full_path: StrPath | None = None) -> pathlib.Path | None: return AF_dir -lib = dotNET() +#: Global variable containing the actual reference to the .NET libraries. +#: The references are only loaded after calling :func:`.load_SDK` function or the +#: :meth:`.load_test_SDK` method. +lib: dotNET = dotNET() def load_SDK(assembly_path: StrPath | None = None) -> None: @@ -103,12 +110,14 @@ def load_SDK(assembly_path: StrPath | None = None) -> None: Parameters ---------- - assembly_path (str | Path, optional): Path to the AF SDK assembly. If None, the default - installation path will be used. + assembly_path : str | pathlib.Path, optional + Path to the directory containing the AF SDK assembly. + If None, the default installation path will be used. Raises ------ - ImportError: If the AF SDK cannot be found or loaded. + ImportError + If the AF SDK cannot be found or loaded. """ global lib lib.load(assembly_path) diff --git a/docs/api/PIconnect.AFSDK.rst b/docs/api/AF/PIconnect.AF.rst similarity index 51% rename from docs/api/PIconnect.AFSDK.rst rename to docs/api/AF/PIconnect.AF.rst index d89267841..0dfa2b924 100644 --- a/docs/api/PIconnect.AFSDK.rst +++ b/docs/api/AF/PIconnect.AF.rst @@ -1,7 +1,7 @@ -PIconnect.AFSDK module -====================== +PIconnect.AF module +===================== -.. automodule:: PIconnect.AFSDK +.. automodule:: PIconnect.AF :members: :undoc-members: :inherited-members: diff --git a/docs/api/PIAF/PIconnect.PIAFAttribute.rst b/docs/api/AF/PIconnect.Asset.rst similarity index 50% rename from docs/api/PIAF/PIconnect.PIAFAttribute.rst rename to docs/api/AF/PIconnect.Asset.rst index 707d027f0..3f2484877 100644 --- a/docs/api/PIAF/PIconnect.PIAFAttribute.rst +++ b/docs/api/AF/PIconnect.Asset.rst @@ -1,7 +1,8 @@ -PIconnect.PIData module +PIconnect.Asset module ======================= -.. automodule:: PIconnect.PIAFAttribute +.. automodule:: PIconnect.Asset :members: :undoc-members: :show-inheritance: + :inherited-members: diff --git a/docs/api/AF/PIconnect.EventFrame.rst b/docs/api/AF/PIconnect.EventFrame.rst new file mode 100644 index 000000000..996450ae4 --- /dev/null +++ b/docs/api/AF/PIconnect.EventFrame.rst @@ -0,0 +1,7 @@ +PIconnect.EventFrame module +=========================== + +.. automodule:: PIconnect.EventFrame + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/PI/PIconnect.PI.rst b/docs/api/PI/PIconnect.PI.rst index 229ea0f85..0e1472f54 100644 --- a/docs/api/PI/PIconnect.PI.rst +++ b/docs/api/PI/PIconnect.PI.rst @@ -5,14 +5,7 @@ PIconnect.PI module :members: :undoc-members: :inherited-members: - :show-inheritance: - :exclude-members: servers, default_server - - .. autoattribute:: servers - :annotation: Dictionary of known servers, as reported by the SDK - .. autoattribute:: default_server - :annotation: Default server, as reported by the SDK .. autoclass:: PIconnect.PI.PIPoint :members: diff --git a/docs/api/PIAF/PIconnect.PIAF.rst b/docs/api/PIAF/PIconnect.PIAF.rst deleted file mode 100644 index 143581f2d..000000000 --- a/docs/api/PIAF/PIconnect.PIAF.rst +++ /dev/null @@ -1,33 +0,0 @@ -PIconnect.PIAF module -===================== - -.. autoclass:: PIconnect.PIAF.PIAFDatabase - :members: - :undoc-members: - :inherited-members: - :show-inheritance: - :exclude-members: servers, default_server - - .. autoattribute:: servers - :annotation: Dictionary of known servers, as reported by the SDK - - .. autoattribute:: default_server - :annotation: Default server, as reported by the SDK - -.. autoclass:: PIconnect.PIAF.PIAFElement - :members: - :undoc-members: - :inherited-members: - :show-inheritance: - -.. autoclass:: PIconnect.PIAF.PIAFAttribute - :members: - :undoc-members: - :inherited-members: - :show-inheritance: - -.. autoclass:: PIconnect.PIAF.PIAFTable - :members: - :undoc-members: - :inherited-members: - :show-inheritance: diff --git a/docs/api/PIconnect.Data.rst b/docs/api/PIconnect.Data.rst new file mode 100644 index 000000000..ffce0806a --- /dev/null +++ b/docs/api/PIconnect.Data.rst @@ -0,0 +1,32 @@ +PIconnect.Data module +===================== + +Data containers +--------------- + +.. autoclass:: PIconnect.Data.DataContainer + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: PIconnect.Data.DataContainerCollection + :members: + :undoc-members: + :show-inheritance: + +AF Enumeration Values +--------------------- + +.. autoclass:: PIconnect.Data.AFEnumerationValue + :members: + :undoc-members: + :show-inheritance: + +Enumerations +------------ + +.. automodule:: PIconnect.Data + :members: + :exclude-members: DataContainer, DataContainerCollection, AFEnumerationValue + :undoc-members: + :show-inheritance: diff --git a/docs/api/PIconnect.PIConsts.rst b/docs/api/PIconnect.PIConsts.rst index f91f08e5e..2a7359dfc 100644 --- a/docs/api/PIconnect.PIConsts.rst +++ b/docs/api/PIconnect.PIConsts.rst @@ -2,5 +2,6 @@ PIconnect.PIConsts module ========================= .. automodule:: PIconnect.PIConsts + :no-index: :members: :undoc-members: diff --git a/docs/api/PIconnect.PIData.rst b/docs/api/PIconnect.Search.rst similarity index 50% rename from docs/api/PIconnect.PIData.rst rename to docs/api/PIconnect.Search.rst index a4280d199..a3b0e02eb 100644 --- a/docs/api/PIconnect.PIData.rst +++ b/docs/api/PIconnect.Search.rst @@ -1,7 +1,8 @@ -PIconnect.PIData module +PIconnect.Search module ======================= -.. automodule:: PIconnect.PIData +.. automodule:: PIconnect.Search :members: :undoc-members: + :inherited-members: :show-inheritance: diff --git a/docs/api/PIconnect._time.rst b/docs/api/PIconnect.Time.rst similarity index 65% rename from docs/api/PIconnect._time.rst rename to docs/api/PIconnect.Time.rst index f4393944d..0b5523e52 100644 --- a/docs/api/PIconnect._time.rst +++ b/docs/api/PIconnect.Time.rst @@ -1,7 +1,7 @@ -PIconnect._time module +PIconnect.Time module ====================== -.. automodule:: PIconnect._time +.. automodule:: PIconnect.Time :members: :undoc-members: :inherited-members: diff --git a/docs/api/PIconnect._collections.rst b/docs/api/PIconnect._collections.rst new file mode 100644 index 000000000..a66c02e75 --- /dev/null +++ b/docs/api/PIconnect._collections.rst @@ -0,0 +1,8 @@ +PIconnect._collections module +============================= + +.. automodule:: PIconnect._collections + :members: + :undoc-members: + :inherited-members: + :show-inheritance: diff --git a/docs/api/PIconnect.config.rst b/docs/api/PIconnect.config.rst index a01c551a8..5cd68a6fd 100644 --- a/docs/api/PIconnect.config.rst +++ b/docs/api/PIconnect.config.rst @@ -2,7 +2,7 @@ PIconnect.config module ======================= .. automodule:: PIconnect.config - :members: + :members: PIConfigContainer, PIConfig :undoc-members: :inherited-members: :show-inheritance: diff --git a/docs/api/PIconnect.dotnet.rst b/docs/api/PIconnect.dotnet.rst new file mode 100644 index 000000000..e6fc6a109 --- /dev/null +++ b/docs/api/PIconnect.dotnet.rst @@ -0,0 +1,11 @@ +PIconnect.dotnet module +======================= + +.. automodule:: PIconnect.dotnet + :members: load_SDK, lib, dotNET + :undoc-members: + :inherited-members: + :show-inheritance: + :ignore-module-all: + :member-order: groupwise + diff --git a/docs/api/index.rst b/docs/api/index.rst index 95e80a5c9..69b5c9a56 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -23,7 +23,7 @@ PI Asset Framework related modules :maxdepth: 4 :glob: - ./PIAF/* + ./AF/* Generic utility modules ----------------------- diff --git a/docs/conf.py b/docs/conf.py index b44562daa..6ca269408 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,7 +14,6 @@ import os import sys -from unittest.mock import MagicMock # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -23,15 +22,6 @@ sys.path.insert(0, os.path.abspath("..")) -class Mock(MagicMock): - @classmethod - def __getattr__(cls, name) -> MagicMock: # type: ignore - return MagicMock() - - -MOCK_MODULES = ["pygtk", "gtk", "gobject", "argparse", "numpy", "pandas"] -sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) - # Get the project root dir, which is the parent dir of this # cwd = os.getcwd() # project_root = os.path.dirname(cwd) @@ -70,14 +60,14 @@ def __getattr__(cls, name) -> MagicMock: # type: ignore # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = ".rst" +source_suffix = {'.rst': 'restructuredtext'} # The master toctree document. master_doc = "index" # General information about the project. project = "PIconnect" -copyright = "2017, Hugo Lapré; Stijn de Jong" +copyright = "2025, Hugo Lapré; Stijn de Jong" author = "Hugo Lapré; Stijn de Jong" # The version info for the project you're documenting, acts as replacement for @@ -89,10 +79,13 @@ def __getattr__(cls, name) -> MagicMock: # type: ignore # The full version, including alpha/beta/rc tags. release = PIconnect.__version__ -extlinks = {"afsdk": ("https://docs.aveva.com/bundle/af-sdk/page/html/%s", "")} +extlinks = { + "afsdk": ("https://docs.aveva.com/bundle/af-sdk/page/html/%s", ""), + "dotnet": ("https://learn.microsoft.com/en-us/dotnet/api/%s?view=netframework-4.8", ""), +} intersphinx_mapping = { - "python": ("https://docs.python.org/3.10", None), + "python": ("https://docs.python.org/3.11", None), "pandas": ("https://pandas.pydata.org/docs", None), } @@ -115,6 +108,8 @@ def __getattr__(cls, name) -> MagicMock: # type: ignore todo_include_todos = True autosummary_generate = True + +autodoc_member_order = "groupwise" # -- Options for HTML output ---------------------------------------------- diff --git a/docs/howto.rst b/docs/howto.rst new file mode 100644 index 000000000..40c8618df --- /dev/null +++ b/docs/howto.rst @@ -0,0 +1,11 @@ +============= +How to guides +============= + +Data extraction +--------------- + +.. toctree:: + :maxdepth: 2 + + howto/summaries diff --git a/docs/tutorials/summaries.rst b/docs/howto/summaries.rst similarity index 87% rename from docs/tutorials/summaries.rst rename to docs/howto/summaries.rst index 74c6fd81c..cfe7346fc 100644 --- a/docs/tutorials/summaries.rst +++ b/docs/howto/summaries.rst @@ -16,7 +16,7 @@ the following code: .. code-block:: python import PIconnect as PI - from PIconnect.PIConsts import SummaryType + from PIconnect.Data import SummaryType with PI.PIServer() as server: points = server.search('*')[0] @@ -31,22 +31,22 @@ multiple summaries over the same time span: .. code-block:: python import PIconnect as PI - from PIconnect.PIConsts import SummaryType + from PIconnect.Data import SummaryType with PI.PIServer() as server: points = server.search('*')[0] data = points.summary('*-14d', '*', SummaryType.MAXIMUM | SummaryType.MINIMUM) print(data) -Similarly, a :any:`PIAFAttribute` also has a :any:`PIAFAttribute.summary` +Similarly, a :any:`AFAttribute` also has a :any:`AFAttribute.summary` method, that works in the same way: .. code-block:: python import PIconnect as PI - from PIconnect.PIConsts import SummaryType + from PIconnect.Data import SummaryType - with PI.PIAFDatabase() as database: + with PI.AFDatabase() as database: key = next(iter(database.children)) element = database.children[key] attribute = next(iter(element.attributes.values())) @@ -71,12 +71,12 @@ There are two possibilities for the timestamp, the beginning of the requested time interval, or the end of the interval. Which to return is specified using the `time_type` argument. To always return the beginning of the interval, you should use the :any:`TimestampCalculation.EARLIEST_TIME` constant from -:any:`PIConsts`: +:any:`Data`: .. code-block:: python import PIconnect as PI - from PIconnect.PIConsts import SummaryType, TimestampCalculation + from PIconnect.Data import SummaryType, TimestampCalculation with PI.PIServer() as server: points = server.search('*')[0] @@ -94,7 +94,7 @@ returns the time at the end of the interval: .. code-block:: python import PIconnect as PI - from PIconnect.PIConsts import SummaryType, TimestampCalculation + from PIconnect.Data import SummaryType, TimestampCalculation with PI.PIServer() as server: points = server.search('*')[0] @@ -118,13 +118,13 @@ time for which it is valid. This period depends on the type of data, whether it is stepped or continuous data. To get an unweighted summary, in which every event has equal weight, the -:any:`CalculationBasis.EVENT_WEIGHTED` constant from the :any:`PIConsts` +:any:`CalculationBasis.EVENT_WEIGHTED` constant from the :any:`Data` module should be used: .. code-block:: python import PIconnect as PI - from PIconnect.PIConsts import CalculationBasis, SummaryType + from PIconnect.Data import CalculationBasis, SummaryType with PI.PIServer() as server: points = server.search('*')[0] @@ -143,7 +143,7 @@ Extracting summaries at regular time intervals Besides extracting a single summary over an entire period of time, it is also possible to extract summaries at fixed intervals within a period of time. This -is done using the :any:`PIPoint.summaries` or :any:`PIAFAttribute.summaries` +is done using the :any:`PIPoint.summaries` or :any:`AFAttribute.summaries` methods. In addition to the singular :py:meth:`summary` method, this takes an `interval` as an argument. The following code extracts the maximum value for each hour within the last 14 days: @@ -151,7 +151,7 @@ each hour within the last 14 days: .. code-block:: python import PIconnect as PI - from PIconnect.PIConsts import SummaryType + from PIconnect.Data import SummaryType with PI.PIServer() as server: points = server.search('*')[0] diff --git a/docs/index.rst b/docs/index.rst index ad65ff037..fe928de40 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,6 +26,7 @@ Contents installation tutorials + howto api/index contributing authors diff --git a/docs/tutorials.rst b/docs/tutorials.rst index bfc090383..6f415e563 100644 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -11,7 +11,8 @@ Basics .. toctree:: tutorials/piserver - tutorials/piaf + tutorials/af + tutorials/search Data extraction --------------- @@ -19,7 +20,6 @@ Data extraction .. toctree:: tutorials/recorded_values tutorials/interpolated_values - tutorials/summaries tutorials/timezones tutorials/event_frames diff --git a/docs/tutorials/piaf.rst b/docs/tutorials/af.rst similarity index 51% rename from docs/tutorials/piaf.rst rename to docs/tutorials/af.rst index 9b0728f16..a718933f5 100644 --- a/docs/tutorials/piaf.rst +++ b/docs/tutorials/af.rst @@ -1,8 +1,8 @@ -############################## -Connecting to a PI AF Database -############################## +########################### +Connecting to a AF Database +########################### -To retrieve data from the PI Asset Framework, the :any:`PIAFDatabase` object +To retrieve data from the PI Asset Framework, the :class:`~.AF.AFDatabase` object should be used. The following code connects to the default database on the default server, and prints its server name: @@ -10,7 +10,9 @@ default server, and prints its server name: import PIconnect as PI - with PI.PIAFDatabase() as database: + PI.load_SDK() + + with PI.AFDatabase() as database: print(database.server_name) The Asset Framework represents a hierarchy of elements, with attributes on the @@ -21,33 +23,39 @@ as follows: import PIconnect as PI - with PI.PIAFDatabase() as database: + PI.load_SDK() + + with PI.AFDatabase() as database: for root in database.children.values(): print("Root element: {r}".format(r=root)) The keys of the dictionary are the names of the elements inside. The following snippet first gets the first key in the dictionary, and the uses that to get -the corresponding :any:`PIAFElement` from the dictionary. Then from this -element its :any:`PIAFAttribute` are extracted: +the corresponding :class:`~.Asset.AFElement` from the dictionary. Then from this +element its :class:`~.Asset.AFAttribute`'s are extracted: .. code-block:: python import PIconnect as PI - with PI.PIAFDatabase() as database: + PI.load_SDK() + + with PI.AFDatabase() as database: key = next(iter(database.children)) element = database.children[key] for attr in element.attributes: print(element.attributes[attr]) To get the data for the last 48 hours from a given attribute you need the -:any:`PIAFAttribute.recorded_values` method: +:meth:`.AFAttribute.recorded_values` method: .. code-block:: python import PIconnect as PI - with PI.PIAFDatabase() as database: + PI.load_SDK() + + with PI.AFDatabase() as database: key = next(iter(database.children)) element = database.children[key] attribute = next(iter(element.attributes.values())) @@ -66,8 +74,8 @@ Finding descendants in the hierarchy ************************************ Whilst it is possible to traverse the hierarchy one at a time, by using the -:any:`PIAFElement.children` dictionaries, it is also possible to get a -further descendant using the :any:`PIAFElement.descendant` method. Assuming +:attr:`.AFElement.children` dictionaries, it is also possible to get a +further descendant using the :meth:`.AFElement.descendant` method. Assuming the database has a root element called `Plant1` with a child element `Outlet`, the latter element could be accessed directly as follows: @@ -75,33 +83,16 @@ the latter element could be accessed directly as follows: import PIconnect as PI - with PI.PIAFDatabase() as database: + PI.load_SDK() + + with PI.AFDatabase() as database: element = database.descendant(r"Plant1\Outlet") .. note:: Elements in the hierarchy are separated by a single backslash `\\`, use either raw strings (using the `r` prefix, as in the example - above) or escape each backslash as `\\\\\\\\`. - -.. _finding_attributes: + above) or escape each backslash as `\\\\`. -*************************************** -Searching attributes based on full path -*************************************** - -To get the direct attribute based on the entire element/attributes path -you can use the :any:`PIAFDatabase.search` method. You can provide a single string or list of strings with -the full path and returns a list of attribute objects. - -.. code-block:: python - - import PIconnect as PI - - with PI.PIAFDatabase() as database: - attributes = database.search([r"Plant1\Outlet|Flow|PV", r"Plant1\Outlet|Flow|SP"]) - -.. note:: Elements in the hierarchy are separated by a single backslash `\\`, - use either raw strings (using the `r` prefix, as in the example - above) or escape each backslash as `\\\\\\\\`. +For a more flexible search mechanism, see :doc:`search`. .. _connect_piaf_database: @@ -109,44 +100,53 @@ the full path and returns a list of attribute objects. Connecting to other servers or databases **************************************** -When no arguments are passed to the :any:`PIAFDatabase` constructor, a +When no arguments are passed to the :class:`~.AF.AFDatabase` constructor, a connection is returned to the default database on the default server. It is possible to connect to other servers or databases, by passing the name of the -server and database as arguments to the :any:`PIAFDatabase` constructor. +server and database as arguments to the :class:`~.AF.AFDatabase` constructor. .. code-block:: python import PIconnect as PI - with PI.PIAFDatabase(server="ServerName", database="DatabaseName") as database: + PI.load_SDK() + + with PI.AFDatabase(server="ServerName", database="DatabaseName") as database: print(database.server_name) -.. note:: It is also possible to specify only server or database. When only - server is specified, a connection to the default database on that server - is returned. Similarly, when only a database is specified, the connection - is made to that database on the default server. +.. note:: + It is also possible to specify only server or database. When only server is + specified, a connection to the default database on that server is returned. + Similarly, when only a database is specified, the connection is made to that + database on the default server. -A list of the available servers can be found in the -:any:`PIAFDatabase.servers` attribute. This is a dictionary, where the keys -are the server names. To get the list of server names you can use the -following code. +A list of the available servers can be found using the +:meth:`~.AF.AFDatabase.servers` classmethod. This is a dictionary, where the keys +are the server names. To get the list of server names you can use the following +code. .. code-block:: python import PIconnect as PI - print(list(PI.PIAFDatabase.servers.keys())) + + PI.load_SDK() + + print(list(PI.AFDatabase.servers().keys())) A list of the databases on a given server can be retrieved from the same -:any:`PIAFDatabase.servers` attribute. Each item in the dictionary of servers -is a dictionary with two items, :data:`server` and :data:`databases`. The -first contains the raw server object from the SDK, while the :data:`databases` -item is a dictionary of {name: object} pairs. So to get the databases for a -given server you can use the following code: +:meth:`~.AF.AFDatabase.servers` attribute. Each item in the dictionary of servers +is a dictionary with two items, ``server`` and ``databases``. The first contains +the raw server object from the SDK, while the ``databases`` item is a dictionary +of ``{name: object}`` pairs. So to get the databases for a given server you can +use the following code: .. code-block:: python import PIconnect as PI - print(list(PI.PIAFDatabase.servers["ServerName"]["databases"].keys())) + + PI.load_SDK() + + print(list(PI.AFDatabase.servers["ServerName"]["databases"].keys())) .. _piaf_tables: @@ -156,16 +156,17 @@ Accessing tables in the PI AF SDK ********************************* It is possible to define custom SQL like tables in the PI AF SDK. -These tables can be accessed using the :any:`PIAFDatabase.tables` attribute. -This attribute is a dictionary of {name: table} pairs. -The table can be loaded into a :any:`pandas.DataFrame` using the -:any:`PIAFTable.data` property: +These tables can be accessed using the :attr:`~.AF.AFDatabase.tables` attribute. +This attribute is a dictionary of ``{name: table}`` pairs. The table can be +loaded into a :class:`pandas.DataFrame` using the :attr:`AFTable.data` property: .. code-block:: python import PIconnect as PI - with PI.PIAFDatabase() as database: + PI.load_SDK() + + with PI.AFDatabase() as database: table = database.tables["MyTable"] df = table.data print(df) diff --git a/docs/tutorials/event_frames.rst b/docs/tutorials/event_frames.rst index fe1201b4d..6c0b1ee3f 100644 --- a/docs/tutorials/event_frames.rst +++ b/docs/tutorials/event_frames.rst @@ -4,22 +4,22 @@ Extracting event frames Since the data in the PI archive is compressed by default, the time interval between consecutive values is typically irregular. To get values at regular -intervals the `interpolated_values` method is used. This is available on both -:any:`PIPoint`, and :any:`PIAFAttribute` objects. +intervals the :meth:`~DataContainer.interpolated_values` method is used. +This is available on both :class:`PIPoint`, and :class:`AFAttribute` objects. -For simplicity this tutorial only uses :any:`PIPoint` objects, see the -tutorial on :doc:`PI AF` to find how to access -:any:`PIAFAttribute` objects. +For simplicity this tutorial only uses :class:`PIPoint` objects, see the +tutorial on :doc:`PI AF` to find how to access +:class:`AFAttribute` objects. *********** Basic usage *********** -The basic example takes the first :any:`PIPoint` that is returned by the +The basic example takes the first :class:`PIPoint` that is returned by the server and gets the data for the last hour at 5 minute intervals, by specifying the `start_time`, `end_time`, and `interval` arguments to -:any:`PIPoint.interpolated_values`: +:meth:`PIPoint.interpolated_values`: .. code-block:: python diff --git a/docs/tutorials/interpolated_values.rst b/docs/tutorials/interpolated_values.rst index 47760f547..d102f5743 100644 --- a/docs/tutorials/interpolated_values.rst +++ b/docs/tutorials/interpolated_values.rst @@ -4,22 +4,22 @@ Extracting interpolated values Since the data in the PI archive is compressed by default, the time interval between consecutive values is typically irregular. To get values at regular -intervals the `interpolated_values` method is used. This is available on both -:any:`PIPoint`, and :any:`PIAFAttribute` objects. +intervals the :meth:`~.DataContainer.interpolated_values` method is used. +This is available on both :class:`PIPoint`, and :class:`PIAFAttribute` objects. -For simplicity this tutorial only uses :any:`PIPoint` objects, see the -tutorial on :doc:`PI AF` to find how to access -:any:`PIAFAttribute` objects. +For simplicity this tutorial only uses :class:`PIPoint` objects, see the +tutorial on :doc:`PI AF` to find how to access +:class:`PIAFAttribute` objects. *********** Basic usage *********** -The basic example takes the first :any:`PIPoint` that is returned by the +The basic example takes the first :class:`PIPoint` that is returned by the server and gets the data for the last hour at 5 minute intervals, by specifying the `start_time`, `end_time`, and `interval` arguments to -:any:`PIPoint.interpolated_values`: +:meth:`PIPoint.interpolated_values`: .. code-block:: python diff --git a/docs/tutorials/recorded_values.rst b/docs/tutorials/recorded_values.rst index 3b69d018a..9891bcdf2 100644 --- a/docs/tutorials/recorded_values.rst +++ b/docs/tutorials/recorded_values.rst @@ -3,13 +3,14 @@ Extracting recorded values ########################## The data in the PI archives are typically compressed [#compression]_. To get the exact values -as they are stored in the archive, the `recorded_values` method should be -used. It is also possible to extract a single historic value using `recorded_value`. -This is available on both :class:`~PIconnect.PI.PIPoint`, and :any:`PIAFAttribute` objects. +as they are stored in the archive, the :meth:`~DataContainer.recorded_values` method should be +used. It is also possible to extract a single historic value using +:meth:`~DataContainer.recorded_value`. This is available on both :class:`~PIPoint`, +and :class:`AFAttribute` objects. -For simplicity this tutorial only uses :class:`~PIconnect.PI.PIPoint` objects, -see the tutorial on :doc:`PI AF` to find how to access -:any:`PIAFAttribute` objects. +For simplicity this tutorial only uses :class:`PIPoint` objects, +see the tutorial on :doc:`PI AF` to find how to access +:class:`AFAttribute` objects. .. [#compression] More information on the compression algorithm can be found in this youtube video: @@ -19,7 +20,7 @@ see the tutorial on :doc:`PI AF` to find how to access Single vs Multiple values ************************* -We start of by extracting a the value from the first :class:`~PIconnect.PI.PIPoint` +We start of by extracting a the value from the first :class:`PIPoint` that is returned by the server as it was 5 minutes ago. .. code-block:: python @@ -31,7 +32,7 @@ that is returned by the server as it was 5 minutes ago. data = point.recorded_value('-5m') print(data) -You will see :any:`PISeries` is printed containing a single row, with the PIPoint name +You will see a :class:`pandas.Series` is printed containing a single row, with the PI tag as the Series name, the point value as the value, and the corresponding timestamp as the index. @@ -42,7 +43,7 @@ the `retrieval_mode` argument to `recorded_value`: .. code-block:: python import PIconnect as PI - from PIconnect.PIConsts import RetrievalMode + from PIconnect.Data import RetrievalMode with PI.PIServer() as server: point = server.search('*')[0] @@ -69,35 +70,32 @@ the `recorded_values` method, and pass a `start_time` and `end_time`: Boundary types ************** -By default only the data between the `start_time` and `end_time` is returned. +By default only the data strictly between the `start_time` and `end_time` is returned. It is also possible to instead return the data from the last value before `start_time` up to and including the first value after `end_time`, by setting -the `boundary_type` to `outside`: +the `boundary_type` to :attr:`BoundaryType.OUTSIDE`: .. code-block:: python import PIconnect as PI + from PIconnect.Data import BoundaryType with PI.PIServer() as server: points = server.search('*')[0] - data = points.recorded_values('*-48h', '*', boundary_type='outside') + data = points.recorded_values('*-48h', '*', boundary_type=BoundaryType.OUTSIDE) print(data) -.. warning:: The :py:data:`boundary_type` argument currently takes a string as - the key to the internal :py:data:`__boundary_types` dictionary. - This will change in a future version to an enumeration in - :any:`PIConsts`. - Finally, it is also possible to interpolate the values surrounding both boundaries such that a value is returned exactly at the requested timestamp: .. code-block:: python import PIconnect as PI + from PIconnect.Data import BoundaryType with PI.PIServer() as server: points = server.search('*')[0] - data = points.recorded_values('*-48h', '*', boundary_type='interpolate') + data = points.recorded_values('*-48h', '*', boundary_type=BoundaryType.INTERPOLATED) print(data) diff --git a/docs/tutorials/search.rst b/docs/tutorials/search.rst new file mode 100644 index 000000000..bec9bafa9 --- /dev/null +++ b/docs/tutorials/search.rst @@ -0,0 +1,3 @@ +############################# +Searching in the AF hierarchy +############################# diff --git a/pixi.lock b/pixi.lock index f9a35207f..319421119 100644 --- a/pixi.lock +++ b/pixi.lock @@ -123,12 +123,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.4-h5888daf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.6.4-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_2.conda @@ -146,7 +146,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.1-h7b32b05_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.0-h7b32b05_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 @@ -163,11 +163,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.9-h9e4cc4f_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.10-h9e4cc4f_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.12-5_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.12-6_cp312.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py312h178313f_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-26.3.0-py312hbf22597_0.conda @@ -305,7 +305,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.13-5_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-307-py313h5813708_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py313h5813708_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.2-py313hb4c8b1a_2.conda @@ -448,11 +448,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py313h46c70d0_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2025.1.31-hbcca054_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py313hfab6e84_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda @@ -463,31 +463,34 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.4-h5888daf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.6.4-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-h4bc722e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-14.2.0-h8f9b012_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py313h8060acc_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py312h178313f_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.1-h7b32b05_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.0-h7b32b05_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.16.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.2-hf636f53_101_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.13-5_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.10-h9e4cc4f_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.12-6_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda @@ -498,19 +501,17 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.13.0-h9fa5a19_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.13.0-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025a-h78e105d_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py313h536fd9c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_1.conda - pypi: https://files.pythonhosted.org/packages/9c/c0/06e64a54bced4e8b885c1e7ec03ee1869e52acf69e87da40f92391a214ad/clr_loader-0.2.7.post0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4b/04/e208ff3ae3ddfbafc05910f89546382f15a3f10186b1f56bd99f159689c2/numpy-2.2.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/e2/e2cbb8d634151aab9528ef7b8bab52ee4ab10e076509285602c2a3a686e0/numpy-2.2.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/cd/f1/bfb6811df4745f92f14c47a29e50e89a36b1533130fcc56452d4660bd2d6/pythonnet-3.0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0f/dd/84f10e23edd882c6f968c21c2434fe67bd4a528967067515feca9e611e5e/tzdata-2025.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: . win-64: - conda: https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.5-pyhd8ed1ab_1.conda @@ -545,10 +546,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.2-h261c0b1_101_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.13-5_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda @@ -559,8 +562,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.13.0-h9fa5a19_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.13.0-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025a-h78e105d_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.3.0-pyhd8ed1ab_0.conda @@ -571,10 +574,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/9c/c0/06e64a54bced4e8b885c1e7ec03ee1869e52acf69e87da40f92391a214ad/clr_loader-0.2.7.post0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/17/d0dd10ab6d125c6d11ffb6dfa3423c3571befab8358d4f85cd4471964fcd/numpy-2.2.4-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cd/f1/bfb6811df4745f92f14c47a29e50e89a36b1533130fcc56452d4660bd2d6/pythonnet-3.0.5-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0f/dd/84f10e23edd882c6f968c21c2434fe67bd4a528967067515feca9e611e5e/tzdata-2025.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl - pypi: . lint: @@ -1085,12 +1086,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.4-h5888daf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.6.4-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-14.2.0-h8f9b012_2.conda @@ -1098,7 +1099,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.1-h7b32b05_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.0-h7b32b05_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pip-25.0.1-pyh8b19718_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_1.conda @@ -1106,9 +1107,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.5-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-6.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.9-h9e4cc4f_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.10-h9e4cc4f_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.12-5_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.12-6_cp312.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda @@ -1162,7 +1163,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.2-h261c0b1_101_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.13-5_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda @@ -2648,6 +2649,19 @@ packages: purls: [] size: 73304 timestamp: 1730967041968 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda + sha256: 33ab03438aee65d6aa667cf7d90c91e5e7d734c19a67aa4c7040742c0a13d505 + md5: db0bfbe7dd197b68ad5f30333bae6ce0 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - expat 2.7.0.* + license: MIT + license_family: MIT + purls: [] + size: 74427 + timestamp: 1743431794976 - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.6.4-he0c23c2_0.conda sha256: 0c0447bf20d1013d5603499de93a16b6faa92d7ead870d96305c0f065b6a5a12 md5: eb383771c680aa792feb529eaf9df82f @@ -2769,6 +2783,16 @@ packages: purls: [] size: 111357 timestamp: 1738525339684 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_0.conda + sha256: f4f21dfc54b08d462f707b771ecce3fa9bc702a2a05b55654f64154f48b141ef + md5: 0e87378639676987af32fee53ba32258 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: 0BSD + purls: [] + size: 112709 + timestamp: 1743771086123 - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.6.4-h2466b09_0.conda sha256: 3f552b0bdefdd1459ffc827ea3bf70a6a6920c7879d22b6bfd0d73015b55227b md5: c48f6ad0ef0a555b27b233dfcab46a90 @@ -2964,22 +2988,6 @@ packages: - pkg:pypi/markupsafe?source=hash-mapping size: 24604 timestamp: 1733219911494 -- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py313h8060acc_1.conda - sha256: d812caf52efcea7c9fd0eafb21d45dadfd0516812f667b928bee50e87634fae5 - md5: 21b62c55924f01b6eef6827167b46acb - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 - constrains: - - jinja2 >=3.0.0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/markupsafe?source=hash-mapping - size: 24856 - timestamp: 1733219782830 - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.2-py313hb4c8b1a_1.conda sha256: f16cb398915f52d582bcea69a16cf69a56dab6ea2fab6f069da9c2c10f09534c md5: ec9ecf6ee4cceb73a0c9a8cdfdf58bed @@ -3218,6 +3226,18 @@ packages: purls: [] size: 2939306 timestamp: 1739301879343 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.0-h7b32b05_0.conda + sha256: 38285d280f84f1755b7c54baf17eccf2e3e696287954ce0adca16546b85ee62c + md5: bb539841f2a3fde210f387d00ed4bb9d + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=13 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3121673 + timestamp: 1744132167438 - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.4.1-ha4e3fda_0.conda sha256: 56dcc2b4430bfc1724e32661c34b71ae33a23a14149866fc5645361cfd3b3a6a md5: 0730f8094f7088592594f9bf3ae62b3f @@ -3729,8 +3749,8 @@ packages: timestamp: 1733327448200 - pypi: . name: piconnect - version: 0.12.3+8.gad40a3b.dirty - sha256: 164cf1763c9eff6525a716677db7139643672af96fe1d4664d1317dd384e8ff5 + version: 1.0.0rc0+5.g468911a.dirty + sha256: 993aff20054a0f31192b3fa4ec23ff6867f300911f81781f2f58554c1c004f17 requires_dist: - pandas>=2,<3 - numpy>=2,<3 @@ -4006,6 +4026,33 @@ packages: purls: [] size: 30594389 timestamp: 1741036299726 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.10-h9e4cc4f_0_cpython.conda + sha256: 4dc1da115805bd353bded6ab20ff642b6a15fcc72ac2f3de0e1d014ff3612221 + md5: a41d26cd4d47092d683915d058380dec + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.7.0,<3.0a0 + - libffi >=3.4.6,<3.5.0a0 + - libgcc >=13 + - liblzma >=5.8.1,<6.0a0 + - libnsl >=2.0.1,<2.1.0a0 + - libsqlite >=3.49.1,<4.0a0 + - libuuid >=2.38.1,<3.0a0 + - libxcrypt >=4.4.36 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.0,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + purls: [] + size: 31279179 + timestamp: 1744325164633 - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.9-h9e4cc4f_1_cpython.conda build_number: 1 sha256: 77f2073889d4c91a57bc0da73a0466d9164dbcf6191ea9c3a7be6872f784d625 @@ -4205,6 +4252,17 @@ packages: purls: [] size: 6238 timestamp: 1723823388266 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.12-6_cp312.conda + build_number: 6 + sha256: 09aff7ca31d1dbee63a504dba89aefa079b7c13a50dae18e1fe40a40ea71063e + md5: 95bd67b1113859774c30418e8481f9d8 + constrains: + - python 3.12.* *_cpython + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 6872 + timestamp: 1743483197238 - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.13-5_cp313.conda build_number: 5 sha256: 438225b241c5f9bddae6f0178a97f5870a89ecf927dfca54753e689907331442 @@ -4889,17 +4947,6 @@ packages: - pkg:pypi/types-python-dateutil?source=hash-mapping size: 22104 timestamp: 1733612458611 -- conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - noarch: python - sha256: c8e9c1c467b5f960b627d7adc1c65fece8e929a3de89967e91ef0f726422fd32 - md5: b6a408c64b78ec7b779a3e5c7a902433 - depends: - - typing_extensions 4.12.2 pyha770c72_1 - license: PSF-2.0 - license_family: PSF - purls: [] - size: 10075 - timestamp: 1733188758872 - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.13.0-h9fa5a19_1.conda sha256: 4dc1002493f05bf4106e09f0de6df57060c9aab97ad709392ab544ceb62faadd md5: 3fbcc45b908040dca030d3f78ed9a212 @@ -4909,17 +4956,6 @@ packages: purls: [] size: 89631 timestamp: 1743201626659 -- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda - sha256: 337be7af5af8b2817f115b3b68870208b30c31d3439bec07bfb2d8f4823e3568 - md5: d17f13df8b65464ca316cbc000a3cb64 - depends: - - python >=3.9 - license: PSF-2.0 - license_family: PSF - purls: - - pkg:pypi/typing-extensions?source=hash-mapping - size: 39637 - timestamp: 1733188758212 - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.13.0-pyh29332c3_1.conda sha256: 18eb76e8f19336ecc9733c02901b30503cdc4c1d8de94f7da7419f89b3ff4c2f md5: 4c446320a86cc5d48e3b80e332d6ebd7 diff --git a/pyproject.toml b/pyproject.toml index a5b197d41..bb3d159ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,7 +103,7 @@ jupyterlab = "*" build = { features = ["build"], no-default-feature = true } test = { features = ["test"], solve-group = "default" } lint = ["lint"] -docs = ["docs"] +docs = { features = ["docs"], solve-group = "default" } py311 = ["py311", "test"] py313 = ["py313", "test"] py312 = ["py312", "test"] diff --git a/tests/test_PI.py b/tests/test_PI.py index 9d2b93fef..de305a670 100644 --- a/tests/test_PI.py +++ b/tests/test_PI.py @@ -7,7 +7,7 @@ import PIconnect as PI import PIconnect.PI as PI_ -from PIconnect import dotnet +from PIconnect import Data, dotnet from .fakes import VirtualTestCase, pi_point @@ -58,7 +58,7 @@ def test_search_single_string(self): """Test searching for PI points using a single string.""" with PI.PIServer() as server: points = server.search("L_140_053*") - assert isinstance(points, list) + assert isinstance(points, Data.DataContainerCollection) for point in points: assert isinstance(point, PI_.PIPoint) @@ -66,7 +66,7 @@ def test_search_multiple_strings(self): """Tests searching for PI points using a list of strings.""" with PI.PIServer() as server: points = server.search(["L_140_053*", "M_127*"]) - assert isinstance(points, list) + assert isinstance(points, Data.DataContainerCollection) for point in points: assert isinstance(point, PI_.PIPoint)