diff --git a/docs/cve_audit.md b/.circleci/cve_audit.md similarity index 100% rename from docs/cve_audit.md rename to .circleci/cve_audit.md diff --git a/README.md b/README.md index 5be9e7f..a60f80a 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ The Sight Machine software development kit (SDK) complements the Sight Machine platform by providing advanced users with the ability to retrieve data from the platform. It is designed primarily for data analysts or data scientists who are interested in retrieving data from Sight Machine so they can perform their own custom analytics. +Documentation: +- **[docs/README.md](docs/README.md)** — full API reference for every `client` method +- **[examples/](examples/)** — runnable Jupyter notebooks demonstrating common workflows (quick start & queries, KPIs, cookbooks) ## Installation diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..7212191 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,1672 @@ +# All Functions + +This document provides an overview of the various functions available in the Sight Machine SDK. These functions are designed to help you interact with and retrieve data from the Sight Machine platform efficiently. Each function is accompanied by example code snippets to demonstrate their usage and to help you integrate them into your own applications. + +Functions are split into general functions which pull general factory metadata, and data queries which pull tabular data from the various data models. All functions are methods of the client object, which needs to be instantiated first. + + +## Table of Contents + +- [Instantiate Client](#instantiate-client) +- [General Metadata Functions](#general-metadata-functions) + - [Machine Type Info](#machine-type-info) + - [Client.get_machine_types](#clientget_machine_types) + - [Client.get_machine_type_names](#clientget_machine_type_names) + - [Client.get_fields_of_machine_type](#clientget_fields_of_machine_type) + - [Machine Info](#machine-info) + - [Client.get_machines](#clientget_machines) + - [Client.get_machine_names](#clientget_machine_names) + - [Client.get_machine_schema](#clientget_machine_schema) + - [Client.get_type_from_machine](#clientget_type_from_machine) + - [Other](#other) + - [Client.get_lines](#clientget_lines) + - [Client.get_machine_timezone](#clientget_machine_timezone) + - [Client.create_share_link](#clientcreate_share_link) + - [Client.select_workspace_id](#clientselect_workspace_id) +- [Data Query Functions](#data-query-functions) + - [Common Query Parameters](#common-query-parameters) + - [Asset Selection Object](#asset-selection-object) + - [Cycle Data](#cycle-data) + - [Client.get_cycles](#clientget_cycles) + - [Parts](#parts) + - [Client.get_part_type_names](#clientget_part_type_names) + - [Client.get_part_schema](#clientget_part_schema) + - [Client.get_parts](#clientget_parts) + - [Downtimes](#downtimes) + - [Client.get_downtimes](#clientget_downtimes) + - [KPIs](#kpis) + - [Client.get_kpis](#clientget_kpis) + - [Client.get_kpis_for_asset](#clientget_kpis_for_asset) + - [Client.get_kpi_data_viz](#clientget_kpi_data_viz) + - [Data Viz Query Object](#data-viz-query-object) + - [Lines](#lines) + - [Client.get_line_data](#clientget_line_data) + - [Client.get_line_data_lineviz](#clientget_line_data_lineviz) + - [Raw Data](#raw-data) + - [Client.get_raw_data](#clientget_raw_data) + - [Cookbooks](#cookbooks) + - [Client.get_cookbooks](#clientget_cookbooks) + - [Client.get_cookbook_top_results](#clientget_cookbook_top_results) + - [Client.get_cookbook_current_value](#clientget_cookbook_current_value) + - [Client.normalize_constraints](#clientnormalize_constraints) + - [Cookbook Object Structure](#cookbook-object-structure) + - [Recipe Run Object Structure](#recipe-run-object-structure) + + + + +## Instantiate Client + +To interact with Sight Machine data, you first need to instantiate a client. This client handles the connection to the Sight Machine API. + +The first step is to import the client submodule from the smsdk package. See the main [README.md](../README.md#installation) for installation instructions. + +```python +from smsdk import client +``` + +Next, create an instance of the Client class. The 'tenant' argument should be set to the part of the tenant URL that preceeds '.sightmachine.io'. + +```python +tenant = 'demo' +cli = client.Client(tenant) +``` + +Finally, provide your API key and secret to initialize the connection. See [README.md -> Authenticating](../README.md#authenticating) for instructions for generating an API key and secret in-platform. The login function will return a boolean value indicating if the connection was successful. If it returns false, a few possible causes are invalid tenant name, incorrect API key and secret for that tenant, or lack of internet connection. + +```python +success = cli.login('apikey', + key_id = api_key, + secret_id = api_secret) +if not success: + raise AssertionError("SDK login failed.") +``` + + + + + +## General Metadata Functions + +Functions in this section are for pulling general factory metadata. + + +### Machine Type Info +--- + + +#### Client.get_machine_types + +Get a list of tags available for each machine type and associated metadata for each tag. Note that this includes extensive internal metadata. If you only want to get a list of available machine types, see get_machine_type_names(). + +```python +cli.get_machine_types(source_type=None, source_type_clean=None) +``` + +Parameters: +> - **source_type**: *str, default None* +> - Machine source_type to filter the output to. Note that this is a a Sight Machine internal machine type, not a UI-based display name. +> - **source_type_clean**: *str, default None* +> - Machine source_type_clean to filter the output to. Note that this is a UI-based display name, not a Sight Machine internal machine type. + +Returns: +> - **Pandas DataFrame** +> - A table with full metadata about each machine type. 25 columns total. + +Examples: + +*Note that these examples are truncated because the output table is too large to display clearly.* + + +See all tag info +```python +>>> df = cli.get_machine_types() +>>> df.shape +(2439, 25) +``` + +Filter to one machine type +```python +>>> df = cli.get_machine_types(source_type_clean="Oven") +>>> df.shape +(142, 25) +``` + + + +#### Client.get_machine_type_names + +Get a list of machine type names. + +```python +cli.get_machine_type_names(clean_strings_out=True) +``` + +Parameters: +> - **clean_strings_out**: *boolean, default True* +> - If true, return the list using the UI-based display names. If false, the list contains the Sight Machine internal machine types. + +Returns: +> - **list** +> - A list of machine types. + +Examples: + + +Machine type display names +```python +>>> cli.get_machine_type_names() +["Oven", "Fryer"] +``` + +Machine type internal names +```python +>>> cli.get_machine_type_names(clean_strings_out=False) +["mt_oven", "mt_fryer"] +``` + + + +#### Client.get_fields_of_machine_type + +Get the available fields and field metadata for a specific machine type. This is similar to get_machine_schema(), but takes a machine type name directly instead of a machine name. Returns a list of dictionaries rather than a DataFrame. + +```python +cli.get_fields_of_machine_type(machine_type=None, types=[], show_hidden=False) +``` + +Parameters: +> - **machine_type**: *str, required* +> - The Sight Machine internal machine type name to get fields for. +> - **types**: *list, default []* +> - A list of data type strings to filter by. If provided, only fields matching one of the specified types will be returned. Common types include "float", "int", "string", "boolean", "datetime". +> - **show_hidden**: *boolean, default False* +> - If true, include fields that are hidden from the UI. By default, UI-hidden fields are excluded. + +Returns: +> - **list** +> - A list of dictionaries, each representing a field. Each dictionary includes keys such as `display_name`, `name`, `type`, `data_type`, `unit`, `stream_types`, and `raw_data_field`. + +Examples: + +Get all fields for a machine type +```python +>>> fields = cli.get_fields_of_machine_type("Lasercut") +>>> len(fields) +142 +>>> fields[0] +{'display_name': 'Machine', 'unit': '', 'type': 'categorical', 'data_type': 'string', 'stream_types': [], 'raw_data_field': '', 'name': 'machine__source'} +``` + +Filter to only numeric fields +```python +>>> fields = cli.get_fields_of_machine_type("Lasercut", types=["float", "int"]) +>>> len(fields) +98 +``` + +Include hidden fields +```python +>>> fields = cli.get_fields_of_machine_type("Lasercut", show_hidden=True) +>>> len(fields) +155 +``` + + + + +### Machine Info +--- + + + + +#### Client.get_machines + +Get a list of all machines and their metadata. Notable metadata items are machine UI-based display name, Sight Machine internal name, machine type, and factory location. If you only want to get a list of available machines, see get_machine_names(). + +```python +cli.get_machines() +``` + +Returns: +> - **Pandas DataFrame** +> - A table with metadata about each machine. There are 10 total columns. + +Examples: + +*Note that this example is truncated because the output table is too large to display clearly.* + +Get all machines +```python +>>> df = cli.get_machines() +>>> df.shape +(20, 10) +``` + + + + +#### Client.get_machine_names + +Get a list of machine names. + +```python +cli.get_machine_names(source_type=None, clean_strings_out=True) +``` + +Parameters: +> - **source_type**: *str, default None* +> - Machine type to filter the output to. This accepts either a UI-based display name (e.g. "Oven") or a Sight Machine internal machine type (e.g. "mt_oven"). The function will first look for an exact internal name match and fall back to a display name lookup. +> - **clean_strings_out**: *boolean, default True* +> - If true, return the list using the UI-based display names. If false, the list contains the Sight Machine internal machine names. + +Returns: +> - **list** +> - A list of machine names. + +Examples: + +Machine display names +```python +>>> cli.get_machine_names() +["Oven_1", "Fryer_2"] +``` + +Machine internal names +```python +>>> cli.get_machine_names(clean_strings_out=False) +["mt_oven_1", "mt_fryer_2"] +``` + +Filter by machine type (display name) +```python +>>> cli.get_machine_names(source_type="Oven") +["Oven_1", "Oven_2"] +``` + +Filter by machine type (internal name) +```python +>>> cli.get_machine_names(source_type="mt_oven") +["Oven_1", "Oven_2"] +``` + + + + + + + +#### Client.get_machine_schema + +Get a table of available tags and tag metadata for a particular machine. Notable metadata items include Sight Machine internal name, display name, and data type. + +```python +cli.get_machine_schema(machine_source=None, types=[], show_hidden=False, return_mtype=False) +``` + +Parameters: +> - **machine_source**: *str, required* +> - UI-based display name of the machine of interest. +> - **types**: *list, default []* +> - A list of data type strings to filter by. If provided, only fields matching one of the specified types will be returned. Common types include "float", "int", "string", "boolean", "datetime". +> - **show_hidden**: *boolean, default False* +> - If true, include fields that are hidden from the UI. By default, UI-hidden fields are excluded. +> - **return_mtype**: *boolean, default False* +> - If true, return a tuple of (machine_type, DataFrame) instead of just the DataFrame. The machine_type is the internal Sight Machine machine type string. + +Returns: +> - **Pandas DataFrame** (default) +> - A table with metadata about each tag available for this machine. Columns include `display`, `name`, `sight_type`, and `type`. +> - **tuple** (if return_mtype=True) +> - A tuple of (machine_type_string, DataFrame). + + +Examples: + +*Note that this example is truncated because the output table is too large to display clearly.* + +Get all tags for a machine +```python +>>> df = cli.get_machine_schema("Blender_1") +>>> df.shape +(158, 15) +``` + +Filter to numeric fields only +```python +>>> df = cli.get_machine_schema("Blender_1", types=["float"]) +>>> df.shape +(120, 15) +``` + +Also return the machine type +```python +>>> machine_type, df = cli.get_machine_schema("Blender_1", return_mtype=True) +>>> machine_type +'mt_blender' +``` + + + +#### Client.get_type_from_machine + +Given a machine's UI-based display name, get the Sight Machine internal machine type. + +```python +cli.get_type_from_machine(machine_source=None) +``` + +Parameters: +> - **machine_source**: *str, required* +> - UI-based display name of the machine of interest. + +Returns: +> - **str** +> - The associated machine type. Note that this is a Sight Machine internal machine type, not a UI-based display name. + +Examples: + +```python +>>> cli.get_type_from_machine("Oven_1") +"mt_oven" +``` + + + + +### Other +--- + +#### Client.get_lines + +Get information about the lines configured for this tenant. Data returned is in a JSON-like structure. + +```python +cli.get_lines() +``` + +Returns: +> - **list** +> - A list of dictionaries, each of which corresponds to a configured line. The dictionary contains line metadata and an ordered list of machines in that line. + + +Examples: + +```python +>>> cli.get_lines() +[{'id': 'line-401a19b5', + 'factory_id': 'sanfrancisco', + 'display_name': 'Line 1', + 'display_order': [], + 'name': 'line-401a19b5', + 'order': 1, + 'machine': [ + {'name': 'Fryer_1', 'id': '1e4436e46df20d049faada54'}, + {'name': 'Oven_1', 'id': '7cd9277327457e26fa4deac2'}] +}] +``` + + + + +#### Client.get_machine_timezone + +Get the timezone that a machine is in. Timezone format is consistent with the [IANA Time Zone Database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) naming convention and is also compatible with the [pytz package](https://pypi.org/project/pytz/). + +```python +cli.get_machine_timezone(machine_source=None) +``` + +Parameters: +> - **machine_source**: *str, required* +> - UI-based display name of the machine of interest. + +Returns: +> - **str** +> - The timezone of the specified machine. + +Examples: + +```python +>>> cli.get_machine_timezone("Oven_1") +'America/Los_Angeles' +``` + + + + +#### Client.create_share_link + +Create a sharelink for a specific static Data Visualization chart. The link opens directly into the Data Visualization page with the specified chart pre-configured. **Note that a link will be generated even if the input values are invalid -- always verify the link works before sharing.** + +```python +cli.create_share_link( + assets=None, + chartType=None, + yAxis=None, + xAxis=X_AXIS_TIME, + model="cycle", + time_selection=ONE_WEEK_RELATIVE, + **kwargs +) +``` + +Parameters: +> - **assets**: *list, required* +> - A list of machine display names (UI-based) to include in the chart. For cycle and KPI models, these are machine names like `["Oven_1", "Oven_2"]`. For line models, this can either be a list of machine names or a dict with `"assets"` and optional `"assetOffsets"` keys. +> - **chartType**: *str, required* +> - The type of chart to render. Options are `"line"`, `"bar"`, `"scatter"`, and `"box"`. +> - **yAxis**: *dict, str, or list* +> - The variable(s) to display on the y-axis. The format depends on the model: +> - **For cycle model**: A dict describing the y-axis field from the Data Visualization API. The field should be a Sight Machine internal tag name. +> - **For KPI model**: A list of KPI names (strings). +> - **For line model**: A dict or list of dicts, each with keys `"field"` (internal tag name), `"machineName"` (display name), and optionally `"machineType"` (will be auto-populated if omitted). +> - **xAxis**: *dict, default X_AXIS_TIME* +> - The variable to display on the x-axis. The default value results in Cycle End Time on the x-axis. For most use cases, the default is appropriate. The default is: `{"unit": "", "type": "datetime", "data_type": "datetime", "stream_types": [], "raw_data_field": "", "id": "endtime", "title": "Time", "isEnabled": True}` +> - **model**: *str, default "cycle"* +> - The Sight Machine data model to source data from. Options are `"cycle"`, `"kpi"`, and `"line"`. +> - **time_selection**: *dict, default ONE_WEEK_RELATIVE* +> - The time range for the chart. Supports both relative and absolute time selections: +> - **Relative**: `{"time_type": "relative", "relative_start": 1, "relative_unit": "week", "ctime_tz": "America/Los_Angeles"}` +> - **Absolute**: `{"time_type": "absolute", "start_time": "2024-11-01T00:00:00", "end_time": "2024-11-08T00:00:00", "time_zone": "America/Los_Angeles"}` +> - **resolution**: *str, default None* +> - The time resolution for the chart. Options are `"second"`, `"minute"`, `"hour"`, `"day"`, `"week"`, `"month"`, and `"year"`. If None, Data Visualization will automatically choose an appropriate resolution. +> - **compareByField**: *str, default None* +> - A tag to color/group results by. Must be a Sight Machine internal tag name, not a UI-based display name. + + +Returns: +> - **str** +> - A URL string that links directly to the specified chart in Data Visualization. + +Examples: + +Create a share link for a cycle line chart over the last week +```python +>>> link = cli.create_share_link( +... assets=["Oven_1"], +... chartType="line", +... yAxis={"id": "stats__Temperature__val", "title": "Temperature"}, +... model="cycle" +... ) +>>> print(link) +'https://demo.sightmachine.io/#/analysis/datavis/s/a1b2c3d4' +``` + +Create a share link with an absolute time range +```python +>>> link = cli.create_share_link( +... assets=["Oven_1", "Oven_2"], +... chartType="scatter", +... yAxis={"id": "stats__Pressure__val", "title": "Pressure"}, +... model="cycle", +... time_selection={ +... "time_type": "absolute", +... "start_time": "2024-11-01T00:00:00", +... "end_time": "2024-11-08T00:00:00", +... "time_zone": "America/Los_Angeles" +... } +... ) +``` + +Create a share link for a KPI chart +```python +>>> link = cli.create_share_link( +... assets=["Oven_1"], +... chartType="bar", +... yAxis=["quality", "oee"], +... model="kpi", +... resolution="day" +... ) +``` + +Create a share link for a line model chart +```python +>>> link = cli.create_share_link( +... assets=["Oven_1", "Fryer_1"], +... chartType="line", +... yAxis=[ +... {"field": "stats__Temperature__val", "machineName": "Oven_1"}, +... {"field": "stats__OilTemp__val", "machineName": "Fryer_1"} +... ], +... model="line" +... ) +``` + + + +#### Client.select_workspace_id + +Set the SDK to pull all data and metadata from a non-production workspace. This setting applies to all future functions run with this client until otherwise specified. By default, the SDK pulls from the production workspace. + +This is useful when working with development or staging pipelines that are not yet published to production. + +```python +cli.select_workspace_id(workspace_id=None) +``` + +Parameters: +> - **workspace_id**: *str or int, required* +> - The ID of the workspace to switch to. This can be found in the Sight Machine platform under pipeline settings. + +Examples: + +Switch to a development workspace +```python +>>> cli.select_workspace_id(workspace_id="ws-12345") +``` + +After calling this, all subsequent data queries will pull from the specified workspace rather than the production workspace. + + + + + + + + +## Data Query Functions + +Functions in this section are for querying tabular data from the common Sight Machine data models: Cycles, Parts, Downtimes, KPIs, Lines, and Raw Data. There is also support for pulling information from Cookbooks. + +Each data model must be configured on the tenant in order to query it. If a model is not set up for your tenant, the corresponding query function will return an error. + +For detailed information about what each data model represents, see the [Sight Machine documentation](https://docs.sightmachine.com). + + +### Common Query Parameters + +The data query functions for Cycles, Parts, and Downtimes share a common set of query parameters that control filtering, pagination, column selection, and sorting. These are passed as keyword arguments (`**kwargs`). + +#### Filtering by Time + +Time filters use the column name followed by a comparison operator suffix: + +> - **End Time__gte**: *datetime* - End time greater than or equal to (i.e., records ending on or after this time) +> - **End Time__lte**: *datetime* - End time less than or equal to +> - **End Time__gt**: *datetime* - End time strictly greater than +> - **End Time__lt**: *datetime* - End time strictly less than +> - **Start Time__gte** / **Start Time__lte** / etc.: Same operators for start time + +Time values should be Python `datetime` objects: +```python +from datetime import datetime + +query = { + 'Machine': 'Oven_1', + 'End Time__gte': datetime(2024, 1, 1), + 'End Time__lte': datetime(2024, 1, 31) +} +``` + +#### Filtering by Value + +Any tag or field can be filtered using operator suffixes: + +> - **field\_\_in**: *list* - Value is in the provided list +> - **field\_\_nin**: *list* - Value is not in the provided list +> - **field\_\_ne**: *value* - Value is not equal to +> - **field\_\_exists**: *bool* - If True, only return records where the field is not null. If False, only return records where the field is null. +> - **field\_\_gte** / **field\_\_lte** / **field\_\_gt** / **field\_\_lt**: Numeric/date comparisons + +```python +# Filter to specific machines +query = {'Machine__in': ['Oven_1', 'Oven_2'], ...} + +# Exclude a product code +query = {'Product_Code__nin': ['ABC', 'DEF'], ...} + +# Only records where a field exists +query = {'Defect_Reason__exists': True, ...} + +# Numeric filter +query = {'output__ne': 0, ...} +``` + +#### Pagination and Selection + +> - **_limit**: *int* - Maximum number of rows to return. If not specified, defaults to 5000 with a warning. Values over 5000 may lead to timeouts. +> - **_offset**: *int, default 0* - Number of rows to skip before returning results. Useful for paginating through large datasets. +> - **_only**: *list* - A list of column names to include in the output. If not specified, the first 50 fields are selected by default (with a warning). Set to `"*"` to select all fields (use with caution on wide tables). +> - **_order_by**: *str* - Column name to sort results by. Prefix with `-` for descending order. + +```python +query = { + 'Machine': 'Oven_1', + 'End Time__gte': datetime(2024, 1, 1), + 'End Time__lte': datetime(2024, 1, 31), + '_only': ['Machine', 'End Time', 'Temperature', 'Pressure'], + '_limit': 1000, + '_offset': 0, + '_order_by': '-End Time' +} +``` + +#### String Cleaning Parameters + +These parameters are available on get_cycles, get_parts, and get_downtimes: + +> - **normalize**: *boolean, default True* - If true, flatten nested data structures in the response into a flat DataFrame. +> - **clean_strings_in**: *boolean, default True* - If true, the function will automatically convert UI-based display names in your query parameters into Sight Machine internal database names before sending the request. +> - **clean_strings_out**: *boolean, default True* - If true, the function will convert Sight Machine internal database column names in the returned DataFrame into UI-based display names. + +In most cases, you should leave these at their defaults. This means you can use display names (like `"Temperature"`) in your queries and the returned DataFrame will also have display name columns. If you need to work with internal names (like `"stats__Temperature__val"`), set both to False. + + +#### Asset Selection Object + +Several functions (`get_kpis_for_asset`, `get_kpi_data_viz`, line functions) take an `asset_selection` dict that tells the API which machines or machine types to operate on. + +Select one or more machine types: +```python +asset_selection = { + "machine_type": ["Lasercut"] +} +``` + +Select specific machines within a machine type: +```python +asset_selection = { + "machine_type": ["Lasercut"], + "machine_source": ["JB_AB_Lasercut_1"] +} +``` + +Both fields accept lists. For KPI-related functions, `machine_type` can be given as either the internal name (`"Lasercut"`) or the UI display name (`"Laser Cutter"`) — the SDK will translate automatically. For maximum safety, use the internal name returned by `get_type_from_machine()`. + + + +### Cycle Data + + +#### Client.get_cycles + +Retrieve cycle (machine) data. Each row represents one production cycle on a machine. The available columns are the tags configured for that machine type on the Sight Machine platform. + +A machine must be specified in the query. If `_only` is not provided, the first 50 fields are selected by default. If `_limit` is not provided, it defaults to 5000. + +```python +cli.get_cycles(**query) +``` + +Parameters: +> - **Machine**: *str, required* +> - The machine display name to query data for. Can also be specified as `machine__source` (internal name). To query multiple machines, use `Machine__in` with a list. +> - All [common query parameters](#common-query-parameters) are supported. +> - **normalize**: *boolean, default True* +> - **clean_strings_in**: *boolean, default True* +> - **clean_strings_out**: *boolean, default True* + +Returns: +> - **Pandas DataFrame** +> - A table of cycle records. Datetime columns (`endtime`, `starttime`) are automatically converted to pandas Timestamp types. + +Examples: + +Basic cycle query +```python +from datetime import datetime + +query = { + 'Machine': 'Oven_1', + 'End Time__gte': datetime(2024, 1, 1), + 'End Time__lte': datetime(2024, 1, 31), + '_order_by': '-End Time', + '_limit': 1000 +} +df = cli.get_cycles(**query) +``` + +Select specific columns +```python +query = { + 'Machine': 'Oven_1', + 'End Time__gte': datetime(2024, 1, 1), + 'End Time__lte': datetime(2024, 1, 7), + '_only': ['Machine', 'End Time', 'Temperature', 'Pressure', 'Output'], + '_limit': 5000, + '_order_by': '-End Time' +} +df = cli.get_cycles(**query) +``` + +Query multiple machines +```python +query = { + 'Machine__in': ['Oven_1', 'Oven_2'], + 'End Time__gte': datetime(2024, 1, 1), + 'End Time__lte': datetime(2024, 1, 7), + '_only': ['Machine', 'End Time', 'Temperature'], + '_limit': 5000 +} +df = cli.get_cycles(**query) +``` + +Filter by a tag value +```python +query = { + 'Machine': 'Oven_1', + 'End Time__gte': datetime(2024, 1, 1), + 'End Time__lte': datetime(2024, 1, 31), + 'output__ne': 0, + '_only': ['Machine', 'End Time', 'Output', 'Temperature'], + '_limit': 5000 +} +df = cli.get_cycles(**query) +``` + + +### Parts + +#### Client.get_part_type_names + +Get a list of available part type names on this tenant. + +```python +cli.get_part_type_names(clean_strings_out=True) +``` + +Parameters: +> - **clean_strings_out**: *boolean, default True* +> - If true, return the list using UI-based display names. If false, the list contains Sight Machine internal part type names. + +Returns: +> - **list** +> - A list of part type name strings. + +Examples: + +```python +>>> cli.get_part_type_names() +["Engine Block", "Transmission Housing"] +``` + +```python +>>> cli.get_part_type_names(clean_strings_out=False) +["engine_block", "transmission_housing"] +``` + + +#### Client.get_part_schema + +Get a table of available fields and field metadata for a particular part type. + +```python +cli.get_part_schema(part_type, types=[]) +``` + +Parameters: +> - **part_type**: *str, required* +> - The part type to get the schema for. Accepts either a display name or internal name. +> - **types**: *list, default []* +> - A list of data type strings to filter by (e.g., `["float", "string"]`). If empty, all fields are returned. + +Returns: +> - **Pandas DataFrame** +> - A table with columns `name`, `display`, and `type` describing each available field. + +Examples: + +```python +>>> schema = cli.get_part_schema("Engine Block") +>>> schema.shape +(85, 3) +>>> schema.head() + name display type +0 stats__Weight__val Weight float +1 stats__Length__val Length float +``` + + +#### Client.get_parts + +Retrieve part data. Each row represents one part record. Parts aggregate data across multiple machines and represent a finished or in-progress product. + +A part type must be specified in the query. If `_only` is not provided, the first 50 fields are selected by default along with standard top-level fields (part type, serial, timestamps, state). If `_limit` is not provided, it defaults to 5000. + +```python +cli.get_parts(**query) +``` + +Parameters: +> - **Part**: *str, required* +> - The part type display name to query. Can also be specified as `type__part_type` (internal name). To query multiple part types, use `Part__in` with a list. +> - All [common query parameters](#common-query-parameters) are supported. +> - **normalize**: *boolean, default True* +> - **clean_strings_in**: *boolean, default True* +> - **clean_strings_out**: *boolean, default True* + +Returns: +> - **Pandas DataFrame** +> - A table of part records. Datetime columns are automatically converted to pandas Timestamp types. + +Examples: + +Basic part query +```python +from datetime import datetime + +query = { + 'Part': 'Engine Block', + 'End Time__gte': datetime(2024, 1, 1), + 'End Time__lte': datetime(2024, 1, 31), + '_order_by': '-End Time', + '_limit': 1000 +} +df = cli.get_parts(**query) +``` + +Select specific columns and filter +```python +query = { + 'Part': 'Engine Block', + 'End Time__gte': datetime(2024, 1, 1), + 'End Time__lte': datetime(2024, 1, 31), + 'Defect_Reason__exists': True, + '_limit': 500, + '_only': ['Part', 'End Time', 'Serial', 'State (Pass / Fail)', 'Weight'] +} +df = cli.get_parts(**query) +``` + + +### Downtimes + +#### Client.get_downtimes + +Retrieve downtime data. Each row represents one downtime event for a machine. + +A machine must be specified in the query. If `_only` is not provided, the default downtime fields are selected: Machine, Start Time, End Time, Duration, Shift, Downtime Reason, Downtime Category, and Downtime Type. If `_limit` is not provided, it defaults to 5000. + +```python +cli.get_downtimes(**query) +``` + +Parameters: +> - **Machine**: *str, required* +> - The machine display name to query downtime data for. Can also be specified as `machine__source`. To query multiple machines, use `Machine__in` with a list. +> - All [common query parameters](#common-query-parameters) are supported. +> - **normalize**: *boolean, default True* +> - **clean_strings_in**: *boolean, default True* +> - **clean_strings_out**: *boolean, default True* + +Returns: +> - **Pandas DataFrame** +> - A table of downtime records. Datetime columns are automatically converted to pandas Timestamp types. + +Examples: + +Basic downtime query +```python +from datetime import datetime + +query = { + 'Machine': 'Oven_1', + 'End Time__gte': datetime(2024, 1, 1), + 'End Time__lte': datetime(2024, 1, 31), + '_order_by': '-End Time', + '_limit': 1000 +} +df = cli.get_downtimes(**query) +``` + +Select specific columns +```python +query = { + 'Machine': 'Oven_1', + 'End Time__gte': datetime(2024, 1, 1), + 'End Time__lte': datetime(2024, 1, 31), + '_only': ['Machine', 'Start Time', 'End Time', 'Duration', 'Downtime Reason'], + '_order_by': '-End Time' +} +df = cli.get_downtimes(**query) +``` + + +### KPIs + +#### Client.get_kpis + +Get a list of all KPIs configured for this tenant. + +```python +cli.get_kpis() +``` + +Returns: +> - **list** +> - A list of dictionaries, each describing an available KPI. Each dictionary includes keys like `name` and `display_name`. + +Examples: + +```python +>>> kpis = cli.get_kpis() +>>> for kpi in kpis[:3]: +... print(kpi['name'], '-', kpi.get('display_name', '')) +quality - Quality +oee - OEE +availability - Availability +``` + + +#### Client.get_kpis_for_asset + +Get the list of KPIs available for a specific asset (machine type and/or machine). + +```python +cli.get_kpis_for_asset(asset_selection=None) +``` + +Parameters: +> - **asset_selection**: *dict, required* +> - A dictionary specifying the asset to get KPIs for. This should have a `machine_type` key and optionally a `machine_source` key. Both display names and internal names are accepted -- display names will be automatically converted. +> - Format: `{"machine_type": ["MachineTypeName"], "machine_source": ["MachineName"]}` + +Returns: +> - **list** +> - A list of dictionaries describing the available KPIs for the specified asset. + +Examples: + +Get KPIs for a machine type +```python +>>> kpis = cli.get_kpis_for_asset( +... asset_selection={ +... 'machine_type': ['Oven'], +... 'machine_source': ['Oven_1'] +... } +... ) +>>> [kpi['name'] for kpi in kpis[:3]] +['quality', 'oee', 'availability'] +``` + + +#### Client.get_kpi_data_viz + +Retrieve KPI data via the Data Visualization API. This function returns aggregated KPI values over time or grouped by independent variables. + +```python +cli.get_kpi_data_viz( + machine_sources=None, + kpis=None, + i_vars=None, + time_selection=None, + **kwargs +) +``` + +Parameters: +> - **machine_sources**: *list, default None* +> - A list of machine display names to query KPI data for. The function automatically resolves machine types from the machine names. +> - **kpis**: *list, default None* +> - A list of KPI name strings to retrieve (e.g., `["quality", "oee"]`). Each KPI is queried with the `"avg"` aggregation by default. +> - **i_vars**: *list, default None* +> - A list of independent variable dictionaries for grouping/bucketing the data. See the [Data Viz Query Object](#data-viz-query-object) section below for the full i_vars format. +> - Common example: `[{"name": "endtime", "time_resolution": "day", "query_tz": "America/Los_Angeles", "output_tz": "America/Los_Angeles"}]` +> - **time_selection**: *dict, default None* +> - The time range for the query. Supports both relative and absolute formats. See the [Data Viz Query Object](#data-viz-query-object) section below for details. + +You can also pass the full Data Viz query structure directly as keyword arguments. See the [Data Viz Query Object](#data-viz-query-object) section below for the complete query format. + +Returns: +> - **list** +> - A list of data points. Each data point is a dictionary with `i_vals` (independent variable values including time bins) and `d_vals` (dependent variable/KPI values with aggregations). + +Examples: + +Get daily KPI values for the last week +```python +data = cli.get_kpi_data_viz( + machine_sources=['Oven_1'], + kpis=['quality', 'oee'], + i_vars=[{ + 'name': 'endtime', + 'time_resolution': 'day', + 'query_tz': 'America/Los_Angeles', + 'output_tz': 'America/Los_Angeles' + }], + time_selection={ + 'time_type': 'relative', + 'relative_start': 1, + 'relative_unit': 'week', + 'ctime_tz': 'America/Los_Angeles' + } +) +``` + +The returned data has the following structure: +```python +[ + { + "i_vals": { + "endtime": {"bin_min": "2024-01-01", "bin_avg": "2024-01-01T12:00:00"} + }, + "d_vals": { + "quality": {"avg": 0.95}, + "oee": {"avg": 0.87} + } + }, + ... +] +``` + + +#### Data Viz Query Object + +`get_kpi_data_viz` and the Lines data-viz functions accept additional Data Viz query fields as keyword arguments. A complete Data Viz query has this shape: + +```python +{ + "asset_selection": { + "machine_source": ["JB_AB_Lasercut_1"], + "machine_type": ["Lasercut"] + }, + "d_vars": [ + {"name": "quality", "aggregate": ["avg"]} + ], + "i_vars": [ + { + "name": "endtime", + "time_resolution": "day", + "query_tz": "America/Los_Angeles", + "output_tz": "America/Los_Angeles", + "bin_strategy": "user_defined2", + "bin_count": 50 + } + ], + "time_selection": { + "time_type": "relative", + "relative_start": 7, + "relative_unit": "year", + "ctime_tz": "America/Los_Angeles" + }, + "where": [], + "db_mode": "sql" +} +``` + +##### asset_selection +See [Asset Selection Object](#asset-selection-object). + +##### d_vars — dependent variables +A list of fields to return, each with an aggregate. The SDK builds this automatically from `get_kpi_data_viz`'s `kpis` argument but you can override it. + +> - **name**: *str* — name of the dependent variable (KPI name or tag name) +> - **aggregate**: *list* — aggregation(s) to apply. Supported: `avg`, `sum`, `min`, `max`. + +##### i_vars — independent variables +Typically a single time-based variable describing how to bin results along the x-axis. + +> - **name**: *str* — typically `"endtime"` +> - **time_resolution**: *str, optional* — one of `year`, `quarter`, `month`, `week`, `day`, `hour`, `minute`, `second` +> - **query_tz**: *str, optional* — time zone the query is interpreted in +> - **output_tz**: *str, optional* — time zone the returned timestamps are expressed in +> - **bin_strategy**: *str, optional* — one of `user_defined2`, `none`, `categorical` +> - **bin_count**: *int, optional* — number of bins to split the data into + +##### time_selection +Defines the time window for the query. Two shapes supported. + +Relative (go back N units from now): +```python +{ + "time_type": "relative", + "relative_start": 7, + "relative_unit": "year", + "ctime_tz": "America/Los_Angeles" +} +``` +- **time_type**: `"relative"` +- **relative_start**: *int* — how many units back +- **relative_unit**: *str* — one of `year`, `month`, `week`, `day`, `hour`, `minute`, `second` +- **ctime_tz**: *str* — time zone + +Absolute (fixed window): +```python +{ + "time_type": "absolute", + "start_time": "2023-02-23T08:00:00.000Z", + "end_time": "2023-03-01T21:35:35.499Z", + "time_zone": "America/Los_Angeles" +} +``` +- **time_type**: `"absolute"` +- **start_time** / **end_time**: ISO-8601 timestamps +- **time_zone**: *str* + +##### where — record filters (optional) +A list of filter clauses that are AND-ed together: +```python +{"name": "type__part_type", "op": "eq", "value": "EngineBlock"} +``` + +> - **name**: *str* — the field to filter on +> - **op**: *str* — the comparison operator (e.g., `eq`, `ne`, `gt`, `gte`, `lt`, `lte`) +> - **value**: any — the value to compare against + +##### db_mode (optional) +Defaults to `"sql"`. A `"mongo"` mode exists but should rarely be needed. + + + +### Lines + +Line functions query data from the Lines data model, which combines data from multiple machines in a production line into a single unified view. + +Note: the get_lines() function listed in the [General Metadata Functions](#clientget_lines) section returns line configuration metadata. The functions below query actual line production data. + + +#### Client.get_line_data + +Retrieve tabular line data. This queries the line data model and returns a flat table similar to cycle data, but spanning multiple machines in a line. + +```python +cli.get_line_data( + assets=None, + fields=[], + time_selection=ONE_DAY_RELATIVE, + asset_time_offset={}, + filters=[], + limit=400, + offset=0 +) +``` + +Parameters: +> - **assets**: *list, required* +> - A list of machine display names that belong to the line you want to query. +> - **fields**: *list, default []* +> - A list of field selection dictionaries specifying which fields to return. Each dictionary should have `"asset"` and `"name"` keys. Example: `[{"asset": "Oven_1", "name": "Temperature"}]`. If empty, default fields are returned. +> - **time_selection**: *dict, default ONE_DAY_RELATIVE* +> - The time range for the query. Supports both relative and absolute formats. Default is the last 24 hours relative. See [Data Viz Query Object](#data-viz-query-object) for details on the format. +> - **asset_time_offset**: *dict, default {}* +> - A dictionary mapping machine names to their time offsets within the line. Each offset is a dict with `"interval"` (number) and `"period"` (unit string, e.g. `"minutes"`). If not specified for a machine, defaults to `{"interval": 0, "period": "minutes"}`. +> - **filters**: *list, default []* +> - A list of filter conditions to narrow results. Each filter follows the where clause format described in the [Data Viz Query Object](#data-viz-query-object). +> - **limit**: *int, default 400* +> - Maximum number of rows to return. +> - **offset**: *int, default 0* +> - Number of rows to skip for pagination. + +Returns: +> - **list** +> - A list of records (dictionaries) containing the requested line data. + +Examples: + +Get line data for two machines over the last day +```python +data = cli.get_line_data( + assets=['Oven_1', 'Fryer_1'], + fields=[ + {'asset': 'Oven_1', 'name': 'Temperature'}, + {'asset': 'Fryer_1', 'name': 'Oil_Temperature'} + ], + time_selection={ + 'time_type': 'relative', + 'relative_start': 1, + 'relative_unit': 'day', + 'ctime_tz': 'America/Los_Angeles' + }, + limit=400 +) +``` + + +#### Client.get_line_data_lineviz + +Retrieve line data via the Line Visualization API. This is an async-based query that returns aggregated line data, similar to what is displayed in the Line Visualization page of the platform. + +```python +cli.get_line_data_lineviz( + assets=None, + d_vars=None, + i_vars=None, + time_selection=ONE_DAY_RELATIVE, + asset_time_offset={}, + filters=[] +) +``` + +Parameters: +> - **assets**: *list, default None* +> - A list of machine display names in the line. +> - **d_vars**: *list, default None* +> - A list of dependent variable dictionaries. See [Data Viz Query Object](#data-viz-query-object) for the d_vars format. +> - **i_vars**: *list, default None* +> - A list of independent variable dictionaries. See [Data Viz Query Object](#data-viz-query-object) for the i_vars format. +> - **time_selection**: *dict, default ONE_DAY_RELATIVE* +> - The time range for the query. Supports both relative and absolute formats. +> - **asset_time_offset**: *dict, default {}* +> - Time offsets for each machine in the line. +> - **filters**: *list, default []* +> - Filter conditions to narrow results. + +Returns: +> - **list** +> - Aggregated line visualization data. + +Examples: + +```python +data = cli.get_line_data_lineviz( + assets=['Oven_1', 'Fryer_1'], + d_vars=[ + {'name': 'stats__Temperature__val', 'aggregate': ['avg']} + ], + i_vars=[{ + 'name': 'endtime', + 'time_resolution': 'hour' + }], + time_selection={ + 'time_type': 'relative', + 'relative_start': 1, + 'relative_unit': 'day', + 'ctime_tz': 'America/Los_Angeles' + } +) +``` + + +### Raw Data + +#### Client.get_raw_data + +Retrieve raw sensor data. Raw data is the unprocessed data collected directly from equipment sensors, before it is aggregated into cycles. This data is typically at a much higher frequency than cycle data. + +```python +cli.get_raw_data( + raw_data_table=None, + fields=[], + time_selection=ONE_DAY_RELATIVE, + limit=400, + offset=0 +) +``` + +Parameters: +> - **raw_data_table**: *str, required* +> - The name of the raw data table to query. This corresponds to the raw data source configured on the platform. +> - **fields**: *list, default []* +> - A list of field name strings to include in the results. Example: `["temperature", "pressure", "speed"]`. If empty, default fields are returned. +> - **time_selection**: *dict, default ONE_DAY_RELATIVE* +> - The time range for the query. Default is the last 24 hours relative. Supports both relative and absolute formats. See [Data Viz Query Object](#data-viz-query-object) for details on the format. +> - **limit**: *int, default 400* +> - Maximum number of rows to return. +> - **offset**: *int, default 0* +> - Number of rows to skip for pagination. + +Returns: +> - **Pandas DataFrame** +> - A table of raw data records. + +Examples: + +Get raw data for the last day +```python +df = cli.get_raw_data( + raw_data_table='oven_sensors', + fields=['temperature', 'pressure', 'humidity'], + time_selection={ + 'time_type': 'relative', + 'relative_start': 1, + 'relative_unit': 'day', + 'ctime_tz': 'America/Los_Angeles' + }, + limit=1000 +) +``` + +Get raw data for a specific time range +```python +df = cli.get_raw_data( + raw_data_table='oven_sensors', + fields=['temperature'], + time_selection={ + 'time_type': 'absolute', + 'start_time': '2024-01-01T00:00:00', + 'end_time': '2024-01-01T06:00:00', + 'time_zone': 'America/Los_Angeles' + }, + limit=5000, + offset=0 +) +``` + + +### Cookbooks + +Cookbook functions interact with the Sight Machine Cookbooks feature, which provides recipe optimization recommendations. + + +#### Client.get_cookbooks + +Get a list of all cookbooks accessible to the logged-in user, including both deployed and undeployed cookbooks. + +```python +cli.get_cookbooks() +``` + +Returns: +> - **list** +> - A list of dictionaries, each representing a cookbook. Each dictionary contains metadata about the cookbook including its name, ID, recipe groups, and deployment status. + +Examples: + +```python +>>> cookbooks = cli.get_cookbooks() +>>> for cb in cookbooks: +... print(cb['name']) +Oven Temperature Optimization +Fryer Oil Quality +``` + + +#### Client.get_cookbook_top_results + +Get the top-performing runs for a specific recipe group within a cookbook. A "run" represents a period of time where conditions matched a recipe's recommended settings. + +```python +cli.get_cookbook_top_results(recipe_group_id=None, limit=10) +``` + +Parameters: +> - **recipe_group_id**: *str, required* +> - The ID of the recipe group to get results for. This can be found in the cookbook metadata returned by get_cookbooks(). +> - **limit**: *int, default 10* +> - The maximum number of top runs to return. + +Returns: +> - **dict** +> - A dictionary with keys `"runs"` (a list of run records) and `"constraint_groups"` (the constraint definitions for the recipe). + +Examples: + +```python +>>> results = cli.get_cookbook_top_results( +... recipe_group_id='rg-abc123', +... limit=5 +... ) +>>> len(results['runs']) +5 +>>> results['runs'][0].keys() +dict_keys(['start_time', 'end_time', 'score', ...]) +``` + + +#### Client.get_cookbook_current_value + +Get the current (most recent) value of specific machine fields. This is useful for checking current conditions against cookbook recipe recommendations. + +```python +cli.get_cookbook_current_value(variables=[], minutes=1440) +``` + +Parameters: +> - **variables**: *list, required* +> - A list of dictionaries, each specifying a field to get the current value for. Each dictionary must have `"asset"` (machine name) and `"name"` (field name) keys. +> - **minutes**: *int, default 1440* +> - The time window (in minutes) to look back when finding the most recent value. Default is 1440 minutes (24 hours). + +Returns: +> - **list** +> - A list of current values corresponding to the requested variables. + +Examples: + +```python +>>> values = cli.get_cookbook_current_value( +... variables=[ +... {'asset': 'Oven_1', 'name': 'Temperature'}, +... {'asset': 'Oven_1', 'name': 'Pressure'} +... ], +... minutes=60 +... ) +>>> values +[{'asset': 'Oven_1', 'name': 'Temperature', 'value': 375.2}, + {'asset': 'Oven_1', 'name': 'Pressure', 'value': 14.7}] +``` + + +#### Client.normalize_constraints + +Convert a list of cookbook constraint range objects into human-readable string representations. This is a utility function for working with cookbook recipe constraints. + +```python +cli.normalize_constraints(constraints) +``` + +Parameters: +> - **constraints**: *list, required* +> - A list of constraint dictionaries. Each dictionary must have `"to"` and `"from"` keys specifying the range bounds, and optionally `"to_is_inclusive"` and `"from_is_inclusive"` boolean keys. + +Returns: +> - **list** +> - A list of strings representing each constraint range. Square brackets `[]` indicate inclusive bounds and parentheses `()` indicate exclusive bounds. + +Examples: + +```python +>>> constraints = [ +... {'to': 100, 'from': 200, 'to_is_inclusive': True, 'from_is_inclusive': True}, +... {'to': 50, 'from': None, 'to_is_inclusive': False, 'from_is_inclusive': False} +... ] +>>> cli.normalize_constraints(constraints) +['[100,200]', '(50,None)'] +``` + + +#### Cookbook Object Structure + +`get_cookbooks()` returns a list of cookbook dicts. Each dict looks like this: + +```python +{ + "hash": "...", + "name": "Oven Temperature Optimization", + "assetNames": ["JB_HM_Diecast_1"], + "key_constraint": { + "field": { + "fieldName": "stats__Cylinders__val", + "machineId": "e2df2b4f115b763f45d04fa2", + "machineName": "JB_HM_Diecast_1", + "machineDisplayName": "Hamilton - Diecast 1", + "fieldType": "categorical", + "machineType": "Diecast", + "fieldDisplayName": "Cylinders", + "fieldUnit": "" + }, + "valueMap": {"4": 1, "6": 0} + }, + "recipe_groups": [...], + "metadata": {"created_by": {...}}, + "updatetime": "2023-03-16 17:48:55.355000", + "assets": [], + "id": "63ab6b263fa4880c06334b03" +} +``` + +Top-level fields: + +> - **hash**: *str* — hash of the cookbook object +> - **name**: *str* — cookbook name +> - **assetNames**: *list* — machine names the cookbook runs against +> - **key_constraint**: *dict* — the field whose value determines which recipe group (product) is used. `valueMap` maps each value to a recipe-group index. +> - **recipe_groups**: *list* — one entry per product (see below) +> - **metadata**: *dict* — `created_by` info (user ID, email, name) +> - **updatetime**: *str* — last-updated timestamp +> - **assets**: *list* — list of assets used in the cookbook +> - **id**: *str* — cookbook ID + +##### Recipe group + +Each entry in `recipe_groups` represents one product within the cookbook: + +```python +{ + "id": "rg-abc", + "values": [1], + "runBoundaries": [], + "maxDuration": {"isEnabled": False, "minimum": 0, "unit": "second"}, + "topRun": 10, + "constraints": [...], + "levers": [...], + "outcomes": [...], + "filters": {"duration": {...}, "recordFilters": []}, + "dateRange": {"value": {...}, "config": {...}}, + "computeDeployedDateRange": None, + "statsCalculationSetting": "default", + "deployed": {...} +} +``` + +> - **id**: *str* — recipe-group ID (pass this to `get_cookbook_top_results`) +> - **values**: *list* — values currently in this group +> - **topRun**: *int* — number of top runs considered when computing lever stats +> - **constraints**: *list* — fields + value ranges used to partition runs +> - **levers**: *list* — fields that vary between recipes (the knobs) +> - **outcomes**: *list* — fields being optimized +> - **filters**: *dict* — duration threshold + record-level filters +> - **dateRange**: *dict* — time window considered when computing recipes +> - **deployed**: *dict* — the deployed version of this recipe group, minus this field +> - **runBoundaries**, **maxDuration**, **computeDeployedDateRange**, **statsCalculationSetting**: currently unused or legacy + +##### Constraints (within a recipe group) + +Each constraint defines the value ranges that delimit runs: + +```python +{ + "asset": "F1_010_BodyMaker_4", + "name": "stats__BM 001: Cans Out__val", + "type": "continuous", + "values": [ + {"from": None, "from_is_inclusive": False, "to": 340, "to_is_inclusive": False}, + {"from": 340, "from_is_inclusive": True, "to": 6000, "to_is_inclusive": True}, + {"from": 6000, "from_is_inclusive": False, "to": None, "to_is_inclusive": False} + ] +} +``` + +> - **asset**: *str* — machine name the constraint field is on +> - **name**: *str* — internal field name +> - **type**: *str* — data type (typically `"continuous"` or `"categorical"`) +> - **values**: *list* — the value bands. Pass these to `normalize_constraints()` to get compact string labels. + +##### Levers and outcomes + +Both `levers` and `outcomes` entries use the same "field descriptor" shape: + +```python +{ + "fieldName": "stats__AluminumTempAvg__val", + "machineId": "e2df2b4f115b763f45d04fa2", + "machineName": "JB_HM_Diecast_1", + "machineDisplayName": "Hamilton - Diecast 1", + "fieldType": "continuous", + "machineType": "Diecast", + "fieldDisplayName": "AluminumTemp - Average", + "fieldUnit": "celsius" +} +``` + +Outcomes wrap that descriptor with optimization metadata: +```python +{ + "field": { ...descriptor as above... }, + "weight": 1, + "optimization_func": "maximize" +} +``` + +> - **weight**: *number* — relative importance vs. other outcomes +> - **optimization_func**: *str* — typically `"maximize"` or `"minimize"` + + +#### Recipe Run Object Structure + +`get_cookbook_top_results()` returns a dict with two views: `runs` (one entry per run) and `constraint_groups` (one entry per recipe, the aggregate view). An individual run looks like: + +```python +{ + "_count": 12, + "_count_muted": 0, + "_duration_seconds": 649.0, + "_earliest": "2022-10-21T00:35:32+00:00", + "_latest": "2022-10-21T00:46:21+00:00", + "_score": 1.0, + "constraint_group_id": "0", + "constraints": [...], + "cookbook": "63ab6b263fa4880c06334b03", + "filters": [], + "i_vals": [ + {"asset": "SHARED", "name": "group", "value": "0"}, + {"asset": "SHARED", "name": "sequence", "value": 2} + ], + "levers": [...], + "outcomes": [...] +} +``` + +Top-level fields: + +> - **_count**: *int* — total records in the run +> - **_count_muted**: *int* — records filtered out +> - **_duration_seconds**: *float* — run duration in seconds +> - **_earliest** / **_latest**: *str* — ISO timestamps bounding the run +> - **_score**: *float* — the score this run achieved under the cookbook's weighting +> - **constraint_group_id**: *str* — which recipe group (product) the run belongs to +> - **constraints**: *list* — the constraint values the run fell into +> - **cookbook**: *str* — parent cookbook ID +> - **filters**: *list* — filters applied to the run +> - **i_vals**: *list* — constraint / run-boundary values used to delimit this run +> - **levers**: *list* — lever values during the run (see below) +> - **outcomes**: *list* — outcome values during the run (see below) + +##### Lever entry +```python +{ + "asset": "JB_HM_Diecast_1", + "d_pos": 2, + "name": "stats__AluminumTempAvg__val", + "value": { + "avg": 659.84, + "count": 9.0, + "max": 671.10, + "min": 653.72, + "var_pop": 29.82 + } +} +``` + +> - **asset**: *str* — machine name +> - **d_pos**: *int* — index of the corresponding dependent variable (internal) +> - **name**: *str* — internal field name +> - **value**: *dict* — measurement stats: `avg`, `count`, `max`, `min`, `var_pop` + +##### Outcome entry +```python +{ + "asset": "JB_HM_Diecast_1", + "d_pos": 0, + "kpi": { + "aggregates": {"Output": "sum", "ScrapQuantity": "sum"}, + "dependencies": {"Output": 9.0, "ScrapQuantity": 0.0}, + "formula": "((Output) / (Output + ScrapQuantity)) * 100 if ((Output + ScrapQuantity) > 0) else None" + }, + "name": "quality", + "value": { + "avg": 100.0, + "count": 100.0, + "max": 100.0, + "min": 100.0, + "normal": 1.0, + "var_pop": 100.0 + } +} +``` + +> - **asset**, **d_pos**, **name**: as with levers +> - **kpi**: *dict, only present when the outcome is a KPI* — shows the KPI's formula, its input aggregations, and the dependency values during this run +> - **value**: *dict* — measurement stats including an additional **normal** field (measure of the distribution's normality) diff --git a/docs/archive/README.md b/docs/archive/README.md new file mode 100644 index 0000000..27dd93f --- /dev/null +++ b/docs/archive/README.md @@ -0,0 +1,14 @@ +# Archived SDK docs and examples + +This directory holds older Sight Machine SDK documentation and example notebooks that predate the current docs. Content here is kept for historical reference only — treat it as potentially stale. + +**For current documentation:** +- [`docs/README.md`](../README.md) — full SDK reference +- [`examples/`](../../examples/) — runnable example notebooks + +## What's in here + +- **`examples/`** — earlier Jupyter notebooks (Quick Start, Query Examples, KPI Examples, Cookbooks Examples, Interactive Notebook). Superseded by the three consolidated notebooks in the top-level `examples/` directory. +- **`entities/`** — Aug 2024 entity-object docs. Overlapping content folded into `docs/README.md`. +- **`commonly_used_data_types/`** — Nov 2023 parameter-object references (asset selection, cookbook, data-viz query, recipe run, workspace). Content folded into `docs/README.md` as inline object-structure subsections. +- **`errors_explained.md`** — minimal legacy error reference; too sparse to maintain. Current error-handling guidance lives in `docs/README.md`. diff --git a/docs/commonly_used_data_types/asset_selection.md b/docs/archive/commonly_used_data_types/asset_selection.md similarity index 100% rename from docs/commonly_used_data_types/asset_selection.md rename to docs/archive/commonly_used_data_types/asset_selection.md diff --git a/docs/commonly_used_data_types/cookbook.md b/docs/archive/commonly_used_data_types/cookbook.md similarity index 100% rename from docs/commonly_used_data_types/cookbook.md rename to docs/archive/commonly_used_data_types/cookbook.md diff --git a/docs/commonly_used_data_types/data_viz_query.md b/docs/archive/commonly_used_data_types/data_viz_query.md similarity index 100% rename from docs/commonly_used_data_types/data_viz_query.md rename to docs/archive/commonly_used_data_types/data_viz_query.md diff --git a/docs/commonly_used_data_types/run.md b/docs/archive/commonly_used_data_types/run.md similarity index 100% rename from docs/commonly_used_data_types/run.md rename to docs/archive/commonly_used_data_types/run.md diff --git a/docs/commonly_used_data_types/workspace.md b/docs/archive/commonly_used_data_types/workspace.md similarity index 100% rename from docs/commonly_used_data_types/workspace.md rename to docs/archive/commonly_used_data_types/workspace.md diff --git a/docs/entities/cookbook.md b/docs/archive/entities/cookbook.md similarity index 100% rename from docs/entities/cookbook.md rename to docs/archive/entities/cookbook.md diff --git a/docs/entities/data_visualization.md b/docs/archive/entities/data_visualization.md similarity index 100% rename from docs/entities/data_visualization.md rename to docs/archive/entities/data_visualization.md diff --git a/docs/entities/kpis.md b/docs/archive/entities/kpis.md similarity index 100% rename from docs/entities/kpis.md rename to docs/archive/entities/kpis.md diff --git a/docs/entities/lines.md b/docs/archive/entities/lines.md similarity index 100% rename from docs/entities/lines.md rename to docs/archive/entities/lines.md diff --git a/docs/entities/machine.md b/docs/archive/entities/machine.md similarity index 100% rename from docs/entities/machine.md rename to docs/archive/entities/machine.md diff --git a/docs/entities/machine_type.md b/docs/archive/entities/machine_type.md similarity index 100% rename from docs/entities/machine_type.md rename to docs/archive/entities/machine_type.md diff --git a/docs/entities/raw_data.md b/docs/archive/entities/raw_data.md similarity index 100% rename from docs/entities/raw_data.md rename to docs/archive/entities/raw_data.md diff --git a/docs/errors_explained.md b/docs/archive/errors_explained.md similarity index 100% rename from docs/errors_explained.md rename to docs/archive/errors_explained.md diff --git a/examples/Cookbooks Examples.ipynb b/docs/archive/examples/Cookbooks Examples.ipynb similarity index 100% rename from examples/Cookbooks Examples.ipynb rename to docs/archive/examples/Cookbooks Examples.ipynb diff --git a/examples/Interactive Notebook.ipynb b/docs/archive/examples/Interactive Notebook.ipynb similarity index 100% rename from examples/Interactive Notebook.ipynb rename to docs/archive/examples/Interactive Notebook.ipynb diff --git a/examples/KPI Examples.ipynb b/docs/archive/examples/KPI Examples.ipynb similarity index 100% rename from examples/KPI Examples.ipynb rename to docs/archive/examples/KPI Examples.ipynb diff --git a/examples/Query Examples.ipynb b/docs/archive/examples/Query Examples.ipynb similarity index 100% rename from examples/Query Examples.ipynb rename to docs/archive/examples/Query Examples.ipynb diff --git a/examples/Quick Start.ipynb b/docs/archive/examples/Quick Start.ipynb similarity index 100% rename from examples/Quick Start.ipynb rename to docs/archive/examples/Quick Start.ipynb diff --git a/examples/1-quick-start-and-queries.ipynb b/examples/1-quick-start-and-queries.ipynb new file mode 100644 index 0000000..9ec2924 --- /dev/null +++ b/examples/1-quick-start-and-queries.ipynb @@ -0,0 +1,1420 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# smsdk Example 1 — Quick Start & Querying Data\n", + "\n", + "End-to-end walkthrough of the most common SDK operations: setting up a client, exploring machines and their schemas, and querying cycles / parts / downtimes / raw data with the full set of query operators.\n", + "\n", + "For full method signatures and parameter reference, see [docs/README.md](../docs/README.md).\n", + "\n", + "**Updated:** April 2026 — consolidates the former `Quick Start.ipynb` and `Query Examples.ipynb`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from smsdk import client\n", + "from datetime import datetime\n", + "import pandas as pd\n", + "import os" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Set before running this notebook.\n", + "tenant = \"demo-bottling\"\n", + "api_key = \"\"\n", + "api_secret = \"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "cli = client.Client(tenant)\n", + "success = cli.login('apikey', key_id=api_key, secret_id=api_secret)\n", + "assert success, 'SDK login failed — check tenant / API key / secret.'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### (Optional) Select a development pipeline / workspace\n", + "By default, the production pipeline is used. To query data from an alternate pipeline, call `select_workspace_id`. The setting persists until the client is re-instantiated." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# cli.select_workspace_id(workspace_id='')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Exploring machines and their schemas\n", + "\n", + "Cycle data is organized by machine, and each machine has a machine type that defines its schema. The typical lookup flow is: machine type → machine → schema." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['Packer',\n", + " 'Filtec',\n", + " 'Line 1',\n", + " 'L1 Full Can Conveyor',\n", + " 'L1 Warmer',\n", + " 'L1 Case Conveyor',\n", + " 'Electricity',\n", + " 'L1-Depal',\n", + " 'Palletizer',\n", + " 'Filler',\n", + " 'Empty Can Conveyor',\n", + " 'Blender_1',\n", + " 'Wrapper']" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# List all machine types (display names)\n", + "types = cli.get_machine_type_names()\n", + "types" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['L1 - Packer #2 (MEAD 1C)',\n", + " 'L1 - Packer #3 (MEAD 1B)',\n", + " 'L1 - Packer #4 (MEAD 1A)']" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# List machines of a specific type\n", + "machine_type = types[0]\n", + "machines = cli.get_machine_names(source_type=machine_type)\n", + "machines" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
namedisplayunitsight_typetypestream_typesraw_data_fieldmodelformattingannotationsui_hiddenui_hidden_machinesui_hidden_facilitiesmachine_typeformula
0machine__sourceMachinecategoricalstring[]cycleNaNNaNNaNNaNNaNNaNNaN
1starttimeCycle Start Timedatetimedatetime[]cycleNaNNaNNaNNaNNaNNaNNaN
2endtimeCycle End Timedatetimedatetime[]cycleNaNNaNNaNNaNNaNNaNNaN
3production_dateProduction Daydatedatetime[]cycleNaNNaNNaNNaNNaNNaNNaN
4totalCycle Time (Net)mscontinuousint[]cycle{'duration': True, 'formatString': 'seconds'}NaNNaNNaNNaNNaNNaN
\n", + "
" + ], + "text/plain": [ + " name display unit sight_type type stream_types \\\n", + "0 machine__source Machine categorical string [] \n", + "1 starttime Cycle Start Time datetime datetime [] \n", + "2 endtime Cycle End Time datetime datetime [] \n", + "3 production_date Production Day date datetime [] \n", + "4 total Cycle Time (Net) ms continuous int [] \n", + "\n", + " raw_data_field model formatting \\\n", + "0 cycle NaN \n", + "1 cycle NaN \n", + "2 cycle NaN \n", + "3 cycle NaN \n", + "4 cycle {'duration': True, 'formatString': 'seconds'} \n", + "\n", + " annotations ui_hidden ui_hidden_machines ui_hidden_facilities machine_type \\\n", + "0 NaN NaN NaN NaN NaN \n", + "1 NaN NaN NaN NaN NaN \n", + "2 NaN NaN NaN NaN NaN \n", + "3 NaN NaN NaN NaN NaN \n", + "4 NaN NaN NaN NaN NaN \n", + "\n", + " formula \n", + "0 NaN \n", + "1 NaN \n", + "2 NaN \n", + "3 NaN \n", + "4 NaN " + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Get the full schema (tag metadata) for one machine\n", + "schema = cli.get_machine_schema(machines[0])\n", + "schema.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
namedisplayunitsight_typetypestream_typesraw_data_fieldformattingmodelannotationsui_hiddenui_hidden_machinesui_hidden_facilitiesmachine_typeformula
0totalCycle Time (Net)mscontinuousint[]{'duration': True, 'formatString': 'seconds'}cycleNaNNaNNaNNaNNaNNaN
1record_timeCycle Time (Gross)mscontinuousint[]{'duration': True, 'formatString': 'seconds'}cycleNaNNaNNaNNaNNaNNaN
2stats__CIP Status Change__valCIP Status Changecontinuousfloat[]CIP Status Change{'is_convertible': False}cycle{}False[][]{'id': '05ec99d0ca304f185d58bfbe', 'name': 'mt...NaN
3stats__Case jam at discharge__valCase jam at dischargecontinuousfloat[]Case jam at discharge{'is_convertible': False}cycle{}False[][]{'id': '05ec99d0ca304f185d58bfbe', 'name': 'mt...NaN
4stats__Deformed Cartons__valDeformed Cartonscontinuousfloat[]Deformed Cartons{'is_convertible': False}cycle{}False[][]{'id': '05ec99d0ca304f185d58bfbe', 'name': 'mt...NaN
\n", + "
" + ], + "text/plain": [ + " name display unit sight_type \\\n", + "0 total Cycle Time (Net) ms continuous \n", + "1 record_time Cycle Time (Gross) ms continuous \n", + "2 stats__CIP Status Change__val CIP Status Change continuous \n", + "3 stats__Case jam at discharge__val Case jam at discharge continuous \n", + "4 stats__Deformed Cartons__val Deformed Cartons continuous \n", + "\n", + " type stream_types raw_data_field \\\n", + "0 int [] \n", + "1 int [] \n", + "2 float [] CIP Status Change \n", + "3 float [] Case jam at discharge \n", + "4 float [] Deformed Cartons \n", + "\n", + " formatting model annotations ui_hidden \\\n", + "0 {'duration': True, 'formatString': 'seconds'} cycle NaN NaN \n", + "1 {'duration': True, 'formatString': 'seconds'} cycle NaN NaN \n", + "2 {'is_convertible': False} cycle {} False \n", + "3 {'is_convertible': False} cycle {} False \n", + "4 {'is_convertible': False} cycle {} False \n", + "\n", + " ui_hidden_machines ui_hidden_facilities \\\n", + "0 NaN NaN \n", + "1 NaN NaN \n", + "2 [] [] \n", + "3 [] [] \n", + "4 [] [] \n", + "\n", + " machine_type formula \n", + "0 NaN NaN \n", + "1 NaN NaN \n", + "2 {'id': '05ec99d0ca304f185d58bfbe', 'name': 'mt... NaN \n", + "3 {'id': '05ec99d0ca304f185d58bfbe', 'name': 'mt... NaN \n", + "4 {'id': '05ec99d0ca304f185d58bfbe', 'name': 'mt... NaN " + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Filter the schema to only numeric (continuous) tags\n", + "schema_numeric = cli.get_machine_schema(machines[0], types=['continuous'])\n", + "schema_numeric.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'America/Los_Angeles'" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Timezone for a given machine — timestamps returned by the API are UTC; use this to convert\n", + "cli.get_machine_timezone(machines[0])" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'mt_packer'" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Look up the internal machine type name from a machine name\n", + "cli.get_type_from_machine(machines[0])" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
namedisplay_nameunittypedata_typestream_typesraw_data_fieldmodelformattingannotationsui_hiddenui_hidden_machinesui_hidden_facilitiesmachine_typeformula
0machine__sourceMachinecategoricalstring[]cycleNaNNaNNaNNaNNaNNaNNaN
1starttimeCycle Start Timedatetimedatetime[]cycleNaNNaNNaNNaNNaNNaNNaN
2endtimeCycle End Timedatetimedatetime[]cycleNaNNaNNaNNaNNaNNaNNaN
3production_dateProduction Daydatedatetime[]cycleNaNNaNNaNNaNNaNNaNNaN
4totalCycle Time (Net)mscontinuousint[]cycle{'duration': True, 'formatString': 'seconds'}NaNNaNNaNNaNNaNNaN
\n", + "
" + ], + "text/plain": [ + " name display_name unit type data_type stream_types \\\n", + "0 machine__source Machine categorical string [] \n", + "1 starttime Cycle Start Time datetime datetime [] \n", + "2 endtime Cycle End Time datetime datetime [] \n", + "3 production_date Production Day date datetime [] \n", + "4 total Cycle Time (Net) ms continuous int [] \n", + "\n", + " raw_data_field model formatting \\\n", + "0 cycle NaN \n", + "1 cycle NaN \n", + "2 cycle NaN \n", + "3 cycle NaN \n", + "4 cycle {'duration': True, 'formatString': 'seconds'} \n", + "\n", + " annotations ui_hidden ui_hidden_machines ui_hidden_facilities machine_type \\\n", + "0 NaN NaN NaN NaN NaN \n", + "1 NaN NaN NaN NaN NaN \n", + "2 NaN NaN NaN NaN NaN \n", + "3 NaN NaN NaN NaN NaN \n", + "4 NaN NaN NaN NaN NaN \n", + "\n", + " formula \n", + "0 NaN \n", + "1 NaN \n", + "2 NaN \n", + "3 NaN \n", + "4 NaN " + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Schema at the machine-type level (vs. machine level). Returns list of dicts.\n", + "type_fields = cli.get_fields_of_machine_type(cli.get_type_from_machine(machines[0]))\n", + "pd.DataFrame(type_fields).head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Querying cycles\n", + "\n", + "`get_cycles` returns a cycle-level DataFrame. Below we walk through the common query operators.\n", + "\n", + "> Update the date ranges in the settings cell below to something that has data in your tenant." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Query settings (edit me)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "start_time = datetime(2023, 4, 1)\n", + "end_time = datetime(2023, 4, 2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Basic cycle query\n", + "- `'End Time__gte'` / `'End Time__lte'` — greater/less-than-or-equal (use `__gt` / `__lt` for strict)\n", + "- `'_order_by'` — prefix with `-` for descending" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "_limit not specified. Maximum of 5000 rows will be returned.\n", + "_only not specified. Selecting first 50 fields.\n", + "Shape: (0, 0)\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
" + ], + "text/plain": [ + "Empty DataFrame\n", + "Columns: []\n", + "Index: []" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "query = {\n", + " 'Machine': machines[0],\n", + " 'End Time__gte': start_time,\n", + " 'End Time__lte': end_time,\n", + " '_order_by': '-End Time',\n", + "}\n", + "df = cli.get_cycles(**query)\n", + "print(f'Shape: {df.shape}')\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Selecting specific columns with `_only`\n", + "\n", + "If you don't specify `_only`, the SDK returns the first ~50 stats plus common metadata. Pass a list of display names to select. `_only='*'` returns every field (slow).\n", + "\n", + "**Note:** columns that are all-null are dropped from the result; don't be surprised if you get fewer columns back than you requested." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "_limit not specified. Maximum of 5000 rows will be returned.\n", + "Shape: (0, 0)\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
" + ], + "text/plain": [ + "Empty DataFrame\n", + "Columns: []\n", + "Index: []" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Grab a few tag display names (skip the first few internal fields)\n", + "tag_cols = cli.get_machine_schema(machines[0])['display'].to_list()[8:13]\n", + "select_cols = ['Machine', 'End Time'] + tag_cols\n", + "\n", + "query = {\n", + " 'Machine': machines[0],\n", + " 'End Time__gte': start_time,\n", + " 'End Time__lte': end_time,\n", + " '_order_by': '-End Time',\n", + " '_only': select_cols,\n", + "}\n", + "df = cli.get_cycles(**query)\n", + "print(f'Shape: {df.shape}')\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Paginating with `_limit` and `_offset`\n", + "Use these together to page through large result sets." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "_only not specified. Selecting first 50 fields.\n", + "Shape: (0, 0)\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
" + ], + "text/plain": [ + "Empty DataFrame\n", + "Columns: []\n", + "Index: []" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "query = {\n", + " 'Machine': machines[0],\n", + " 'End Time__gte': start_time,\n", + " 'End Time__lte': end_time,\n", + " '_order_by': '-End Time',\n", + " '_offset': 10,\n", + " '_limit': 500,\n", + "}\n", + "df = cli.get_cycles(**query)\n", + "print(f'Shape: {df.shape}')\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Filtering with `__in` / `__nin` (list membership)\n", + "Use `__in` when you want several values, or `__nin` to exclude them. Most common use case: pulling data across several machines of the same type.\n", + "\n", + "**Tip:** don't mix machine types in one call — the returned DataFrame will be sparse and hard to read." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "_limit not specified. Maximum of 5000 rows will be returned.\n", + "_only not specified. Selecting first 50 fields.\n", + "Shape: (0, 0) — Machine column should have up to 3 distinct values\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
" + ], + "text/plain": [ + "Empty DataFrame\n", + "Columns: []\n", + "Index: []" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "query = {\n", + " 'Machine__in': machines[:3],\n", + " 'End Time__gte': start_time,\n", + " 'End Time__lte': end_time,\n", + " '_order_by': '-End Time',\n", + "}\n", + "df = cli.get_cycles(**query)\n", + "print(f'Shape: {df.shape} — Machine column should have up to 3 distinct values')\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Filtering with `__exists` (null / not-null)\n", + "Common for sparse inspection / defect fields. Pass `True` to require the field, `False` to require it be missing." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "_limit not specified. Maximum of 5000 rows will be returned.\n", + "Shape: (0, 0)\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
" + ], + "text/plain": [ + "Empty DataFrame\n", + "Columns: []\n", + "Index: []" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Replace 'Alarms' below with a real sparse field from your tenant's schema\n", + "query = {\n", + " 'Machine': machines[0],\n", + " 'production_date__exists': False,\n", + " 'End Time__gte': start_time,\n", + " 'End Time__lte': end_time,\n", + " '_order_by': '-End Time',\n", + " '_only': ['Machine', 'End Time', 'production_date'],\n", + "}\n", + "df = cli.get_cycles(**query)\n", + "print(f'Shape: {df.shape}')\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Inequality with `__ne`" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "_limit not specified. Maximum of 5000 rows will be returned.\n", + "Shape: (0, 0)\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
" + ], + "text/plain": [ + "Empty DataFrame\n", + "Columns: []\n", + "Index: []" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Replace 'output' with a real numeric tag from your tenant's schema\n", + "query = {\n", + " 'Machine__in': machines[:3],\n", + " 'output__ne': 0,\n", + " 'End Time__gte': start_time,\n", + " 'End Time__lte': end_time,\n", + " '_order_by': '-End Time',\n", + " '_only': ['Machine', 'End Time', 'output'],\n", + "}\n", + "df = cli.get_cycles(**query)\n", + "print(f'Shape: {df.shape}')\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Querying downtimes\n", + "\n", + "`get_downtimes` takes the same query operators as `get_cycles`." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "_limit not specified. Maximum of 5000 rows will be returned.\n", + "Shape: (0, 0)\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
" + ], + "text/plain": [ + "Empty DataFrame\n", + "Columns: []\n", + "Index: []" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "query = {\n", + " 'Machine': machines[0],\n", + " 'End Time__gte': start_time,\n", + " 'End Time__lte': end_time,\n", + " '_order_by': '-End Time',\n", + "}\n", + "df = cli.get_downtimes(**query)\n", + "print(f'Shape: {df.shape}')\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Downtime Pareto — top reasons by total duration\n", + "Simple grouping example showing how to turn a downtime query into a quick summary." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "No downtime rows / expected columns missing — adjust the date range above.\n" + ] + } + ], + "source": [ + "if not df.empty and 'Downtime Reason' in df.columns and 'Duration' in df.columns:\n", + " pareto = (\n", + " df.groupby('Downtime Reason')['Duration']\n", + " .sum()\n", + " .sort_values(ascending=False)\n", + " .head(10)\n", + " )\n", + " display(pareto)\n", + "else:\n", + " print('No downtime rows / expected columns missing — adjust the date range above.')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Querying parts\n", + "\n", + "Parts track an object across multiple machines. The lookup flow is part type → part data — one step simpler than cycles. Query operators are the same." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "# part_types = cli.get_part_type_names()\n", + "# part_types" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Pull a tiny sample first to discover the available column display names for this part type. (`cli.get_part_schema(part_type)` gives the full field list, but has a pandas-2 compat bug as of April 2026 — sampling works as a substitute.)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "# part_type = part_types[0]\n", + "# sample = cli.get_parts(Part=part_type, _limit=1)\n", + "# part_columns = sample.columns.to_list()\n", + "# part_columns[:10]" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "# query = {\n", + "# 'Part': part_type,\n", + "# 'End Time__gte': start_time,\n", + "# 'End Time__lte': end_time,\n", + "# '_limit': 10\n", + "# }\n", + "# df = cli.get_parts(**query)\n", + "# print(f'Shape: {df.shape}')\n", + "# df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Querying raw data\n", + "\n", + "`get_raw_data` pulls from a raw-data table (pre-cycle sensor stream). You must know the raw-data table name and the field names you want; the API returns a list of records.\n", + "\n", + "See [docs/README.md — Raw Data](../docs/README.md#raw-data) for the full parameter list." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "# Replace with a real raw-data table / field names from your tenant.\n", + "# time_selection uses the same format as KPI data-viz queries (see notebook 2).\n", + "time_selection = {\n", + " 'time_type': 'absolute',\n", + " 'start_time': start_time.isoformat() + 'Z',\n", + " 'end_time': end_time.isoformat() + 'Z',\n", + " 'time_zone': 'America/Los_Angeles',\n", + "}\n", + "\n", + "# raw_records = cli.get_raw_data(\n", + "# raw_data_table='',\n", + "# fields=['', ''],\n", + "# time_selection=time_selection,\n", + "# limit=100,\n", + "# )\n", + "# pd.DataFrame(raw_records).head()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/2-kpis.ipynb b/examples/2-kpis.ipynb new file mode 100644 index 0000000..df52f79 --- /dev/null +++ b/examples/2-kpis.ipynb @@ -0,0 +1,743 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# smsdk Example 2 — KPIs\n", + "\n", + "Listing KPIs, finding which KPIs apply to a given asset, and pulling KPI values over time via the data-visualization API.\n", + "\n", + "For full method signatures and parameter reference, see [docs/README.md](../docs/README.md#kpis).\n", + "\n", + "**Updated:** April 2026 — replaces the former `KPI Examples.ipynb`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "from smsdk import client\n", + "import pandas as pd\n", + "import os" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Set before running this notebook.\n", + "tenant = \"demo-bottling\"\n", + "api_key = \"\"\n", + "api_secret = \"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "cli = client.Client(tenant)\n", + "success = cli.login('apikey', key_id=api_key, secret_id=api_secret)\n", + "assert success, 'SDK login failed — check tenant / API key / secret.'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## List all available KPIs\n", + "\n", + "`get_kpis()` returns every KPI defined on the tenant, regardless of asset." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total KPIs: 16\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
nameunitformularollupsfield_mapsdependenciesdisplay_namebad_thresholddirectionalitygood_threshold
0availability%(up_time/all_time)*100{'line': {}, 'factory': {'ETL3_sanfrancisco': ...[{'alias_map': {'up_time': {'row_formula': \"if...[{'name': 'up_time', 'aggregate': 'sum'}, {'na...Availability60positive80
1Line Efficiency%(prod/target)*100{'line': {}, 'factory': {'ETL3_sanfrancisco': ...[{'alias_map': {'prod': {'name': 'stats__Total...[{'name': 'prod', 'aggregate': 'sum'}, {'name'...Line Efficiency60positive80
2MTBFminutesMTBF{'line': {}, 'factory': {'ETL3_sanfrancisco': ...[{'alias_map': {'MTBF': {'name': 'stats__MTBF_...[{'name': 'MTBF', 'aggregate': 'avg'}]MTBF5positive10
3rejectrate%((rejectsl1 + rejectsl2) / (throughputl1 + thr...{'line': {}, 'factory': {'ETL3_sanfrancisco': ...[{'alias_map': {'rejectsl1': {'name': 'stats__...[{'name': 'rejectsl1', 'aggregate': 'sum'}, {'...Reject Rate10negative5
4percentage_of_time_in_micro_stops%((Duration_Rejects/Record_Duration)*100) if (R...{'line': {}, 'factory': {}}[{'alias_map': {'Record_Duration': {'name': 's...[{'name': 'Record_Duration', 'aggregate': 'sum...% Micro Stops40negative20
\n", + "
" + ], + "text/plain": [ + " name unit \\\n", + "0 availability % \n", + "1 Line Efficiency % \n", + "2 MTBF minutes \n", + "3 rejectrate % \n", + "4 percentage_of_time_in_micro_stops % \n", + "\n", + " formula \\\n", + "0 (up_time/all_time)*100 \n", + "1 (prod/target)*100 \n", + "2 MTBF \n", + "3 ((rejectsl1 + rejectsl2) / (throughputl1 + thr... \n", + "4 ((Duration_Rejects/Record_Duration)*100) if (R... \n", + "\n", + " rollups \\\n", + "0 {'line': {}, 'factory': {'ETL3_sanfrancisco': ... \n", + "1 {'line': {}, 'factory': {'ETL3_sanfrancisco': ... \n", + "2 {'line': {}, 'factory': {'ETL3_sanfrancisco': ... \n", + "3 {'line': {}, 'factory': {'ETL3_sanfrancisco': ... \n", + "4 {'line': {}, 'factory': {}} \n", + "\n", + " field_maps \\\n", + "0 [{'alias_map': {'up_time': {'row_formula': \"if... \n", + "1 [{'alias_map': {'prod': {'name': 'stats__Total... \n", + "2 [{'alias_map': {'MTBF': {'name': 'stats__MTBF_... \n", + "3 [{'alias_map': {'rejectsl1': {'name': 'stats__... \n", + "4 [{'alias_map': {'Record_Duration': {'name': 's... \n", + "\n", + " dependencies display_name \\\n", + "0 [{'name': 'up_time', 'aggregate': 'sum'}, {'na... Availability \n", + "1 [{'name': 'prod', 'aggregate': 'sum'}, {'name'... Line Efficiency \n", + "2 [{'name': 'MTBF', 'aggregate': 'avg'}] MTBF \n", + "3 [{'name': 'rejectsl1', 'aggregate': 'sum'}, {'... Reject Rate \n", + "4 [{'name': 'Record_Duration', 'aggregate': 'sum... % Micro Stops \n", + "\n", + " bad_threshold directionality good_threshold \n", + "0 60 positive 80 \n", + "1 60 positive 80 \n", + "2 5 positive 10 \n", + "3 10 negative 5 \n", + "4 40 negative 20 " + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "kpis_dict = cli.get_kpis()\n", + "df_kpis = pd.DataFrame(kpis_dict)\n", + "print(f'Total KPIs: {len(df_kpis)}')\n", + "df_kpis.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## List KPIs available for a specific asset\n", + "\n", + "`get_kpis_for_asset(asset_selection=...)` narrows the list to KPIs actually configured for a given machine type or a specific machine.\n", + "\n", + "> **Note:** `asset_selection` expects the **raw machine-type name** (system name), not the UI display name. The SDK's `get_kpis_for_asset` will attempt to translate a display name, but using the raw name is safer — see the helper below.\n", + "\n", + "For details on the `asset_selection` object, see [docs/README.md — Common Query Parameters](../docs/README.md#common-query-parameters)." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using machine type: Packer\n" + ] + } + ], + "source": [ + "# Helper: display name -> raw machine type\n", + "def get_raw_machine_type(clean_machine_type: str) -> str:\n", + " machines = cli.get_machine_names(source_type=clean_machine_type)\n", + " return cli.get_type_from_machine(machines[0])\n", + "\n", + "# Pick a machine type that exists on your tenant (adjust as needed)\n", + "clean_type = cli.get_machine_type_names()[0]\n", + "print(f'Using machine type: {clean_type}')" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
namedisplay_nameunittypedata_typestream_typesraw_data_fieldmodel
0availabilityAvailability%continuousfloat[]kpi
\n", + "
" + ], + "text/plain": [ + " name display_name unit type data_type stream_types \\\n", + "0 availability Availability % continuous float [] \n", + "\n", + " raw_data_field model \n", + "0 kpi " + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# KPIs available for all machines of a given type\n", + "asset_selection = {\n", + " 'asset_selection': {\n", + " 'machine_type': [get_raw_machine_type(clean_type)],\n", + " },\n", + "}\n", + "kpis_for_type = cli.get_kpis_for_asset(**asset_selection)\n", + "pd.DataFrame(kpis_for_type)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
namedisplay_nameunittypedata_typestream_typesraw_data_fieldmodel
0availabilityAvailability%continuousfloat[]kpi
\n", + "
" + ], + "text/plain": [ + " name display_name unit type data_type stream_types \\\n", + "0 availability Availability % continuous float [] \n", + "\n", + " raw_data_field model \n", + "0 kpi " + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# KPIs available for one specific machine\n", + "machines = cli.get_machine_names(source_type=clean_type)\n", + "asset_selection = {\n", + " 'asset_selection': {\n", + " 'machine_type': [get_raw_machine_type(clean_type)],\n", + " 'machine_source': [machines[0]],\n", + " },\n", + "}\n", + "kpis_for_machine = cli.get_kpis_for_asset(**asset_selection)\n", + "pd.DataFrame(kpis_for_machine)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## KPI values over time (`get_kpi_data_viz`)\n", + "\n", + "`get_kpi_data_viz` is the programmatic equivalent of a Data Viz chart in MA. You specify:\n", + "- **`machine_sources`** — list of machine names\n", + "- **`kpis`** — list of KPI names (averaged by default)\n", + "- **`i_vars`** — independent variables (typically one time field with a resolution)\n", + "- **`time_selection`** — relative or absolute time window\n", + "\n", + "See [docs/README.md — KPIs](../docs/README.md#kpis) for the full parameter schema." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Query settings (edit me)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "KPIs to query: ['availability']\n" + ] + } + ], + "source": [ + "this_machine_sources = [machines[0]]\n", + "\n", + "# IMPORTANT: only pass KPIs that are actually configured for this machine's type.\n", + "# Passing every tenant-level KPI will error out on the first one without a field_map\n", + "# for this machine type (e.g. \"Line Efficiency\" isn't configured for a Packer).\n", + "# `kpis_for_machine` was computed in the prior cell from cli.get_kpis_for_asset().\n", + "this_kpis = [k['name'] for k in kpis_for_machine]\n", + "print(f'KPIs to query: {this_kpis}')\n", + "\n", + "# Time resolution: year | quarter | month | week | day | hour | minute | second\n", + "this_i_vars = [\n", + " {\n", + " 'name': 'endtime',\n", + " 'time_resolution': 'day',\n", + " 'query_tz': 'America/Los_Angeles',\n", + " 'output_tz': 'America/Los_Angeles',\n", + " 'bin_strategy': 'user_defined2',\n", + " 'bin_count': 50,\n", + " },\n", + "]\n", + "\n", + "# Relative time window (last N units). Swap to absolute below if you want a fixed range.\n", + "this_time_selection = {\n", + " 'time_type': 'relative',\n", + " 'relative_start': 1,\n", + " 'relative_unit': 'year',\n", + " 'ctime_tz': 'America/Los_Angeles',\n", + "}\n", + "\n", + "# Alternative: absolute window\n", + "# this_time_selection = {\n", + "# 'time_type': 'absolute',\n", + "# 'start_time': '2023-02-23T08:00:00.000Z',\n", + "# 'end_time': '2023-03-01T21:35:35.499Z',\n", + "# 'time_zone': 'America/Los_Angeles',\n", + "# }" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Points returned: 103\n" + ] + } + ], + "source": [ + "data_dict = cli.get_kpi_data_viz(\n", + " machine_sources=this_machine_sources,\n", + " kpis=this_kpis,\n", + " i_vars=this_i_vars,\n", + " time_selection=this_time_selection,\n", + ")\n", + "\n", + "print(f'Points returned: {len(data_dict)}')\n", + "if data_dict:\n", + " data_dict[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Turn the nested response into a flat DataFrame\n", + "Each returned point has `i_vals` (independent-variable bin info), `d_vals` (KPI aggregates), and `kpi_dependencies` (raw tags feeding the KPI). Flatten to one row per time bin for plotting." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
endtimeavailabilityup_timeall_time
02026-01-01 08:00:00+00:0076.48617466107000.086430000.0
12026-01-02 08:00:00+00:0084.76149773228000.086393000.0
22026-01-03 08:00:00+00:0082.51591371302000.086410000.0
32026-01-04 08:00:00+00:0076.84393366397000.086405000.0
42026-01-05 08:00:00+00:0078.52735267828000.086375000.0
\n", + "
" + ], + "text/plain": [ + " endtime availability up_time all_time\n", + "0 2026-01-01 08:00:00+00:00 76.486174 66107000.0 86430000.0\n", + "1 2026-01-02 08:00:00+00:00 84.761497 73228000.0 86393000.0\n", + "2 2026-01-03 08:00:00+00:00 82.515913 71302000.0 86410000.0\n", + "3 2026-01-04 08:00:00+00:00 76.843933 66397000.0 86405000.0\n", + "4 2026-01-05 08:00:00+00:00 78.527352 67828000.0 86375000.0" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "rows = []\n", + "for point in data_dict:\n", + " row = {'endtime': point['i_vals']['endtime']['bin_avg']}\n", + " for kpi_name, vals in point['d_vals'].items():\n", + " row[kpi_name] = vals['avg']\n", + " for kpi, deps in point['kpi_dependencies'].items():\n", + " for field, value in deps.items():\n", + " row[field] = value\n", + " rows.append(row)\n", + "\n", + "df_trend = pd.DataFrame(rows)\n", + "if not df_trend.empty:\n", + " # bin_avg may come back as unix ms OR as ISO timestamps with mixed offsets,\n", + " # depending on time_resolution / output_tz — let pandas infer and normalize to UTC.\n", + " df_trend['endtime'] = pd.to_datetime(df_trend['endtime'], utc=True)\n", + " df_trend = df_trend.sort_values('endtime').reset_index(drop=True)\n", + "df_trend.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Quick trend plot\n", + "Plots the first numeric KPI column from the flattened table, if any data came back." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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xB5w7M8991x5vb/44oueM8880zNkyBB5/PWo8+auu+7yDBw40PPJJ59oz/F6dwYLFizwTJo0yZOWluYZNmyYZ9q0aS2u+RUrVnhmzpwpr3d1ze/atUv+f7jv63+WJAZek+6A1yMxAq9397AgBdZgVrrNTWBIky00+d94443i97//vayUIiOzevVqKSevrq6WP6skEKiiwgEbvZ7oDX7nnXeS0gbfbfzwww/yGP3nP/8RkyZNEo888oiULmVkZGiVS/RwI/OGTByk5HgcvbvLli2TTuZKZkzcy4YNG8ScOXPEgw8+KM477zzx2muviYceekhMnz5dflao6x0ZWZwXuCdAEQGVyxNPPJHAv4Aopk2bJo/NCy+8ILp16yY/Q9qmv+YxdWLs2LHyelfXfLt27aTETd0fSGLhNekOeD0SI/B6dw/TUmENTnTU7za2bt3qSU9P98yfP1/LwoGXX37Zk52d7fn666+1n1VZmi+++EJWyoqLiz1ZWVmeO++8M0GvnqiqY2Vlpaxig3fffVeqFe69916/4xaM77//3jN27FjP+++/zzczBVi+fLnMyq5bt87v8XvuucdTVFTk97jKwL755pueDh06eLp37+7JzMz0PProo7a/btKSLVu2aPfnhx9+2DNmzBjPCy+8EFGN8OWXX0olE659knh4TboDXo/ECLze3cOWFFiDWek2GWRpYJL0/PPPy+9V1uWcc86Rxluq5wDZGWRp1q1bJ3uCV61aJX7729+KrVu3iltvvdXsl0UMoiqSML9DFRvg8xFHHCFeffVVebxw3BobG7X/B+Zpn3zyiezRP/bYY2Uv/9FHH833PAXAeQAzD/QW6bniiitk9hUKCfVzuBfg/EEVHIoYnFMwALnqqqsS9OqJHrigYrIEOOWUU+SkCdybcYxwX1CZdgA1y9dffy2uueYaceqpp8p+fprjOQNek+6A1yMxAq9391CSAmswg26TadWqlZg4caKUnC5evFieKJA8gD//+c/SRKmyslJztn755Zfl2LBZs2ZJKTM26sRZLQPFxcUyIVJUVCTuvfde+bhewgKZMI7dkiVLpBs95MKUlruDSEZnWBQGDBggr1/ImwAWBhgmXnbZZVJGjikG6nxBEI57AK93+zFqWofj17VrVzlRAoYtGAcI9NMIFi5cKP72t7/JsSVIuKG9gGPgnAGvSXfB65GE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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "if not df_trend.empty:\n", + " numeric_cols = [c for c in df_trend.columns if c != 'endtime' and pd.api.types.is_numeric_dtype(df_trend[c])]\n", + " if numeric_cols:\n", + " first_kpi = numeric_cols[0]\n", + " fig, ax = plt.subplots(figsize=(10, 4))\n", + " df_trend.plot(x='endtime', y=first_kpi, ax=ax, marker='o', legend=False)\n", + " ax.set_title(f'{first_kpi} over time — {this_machine_sources[0]}')\n", + " ax.set_ylabel(first_kpi)\n", + " plt.tight_layout()\n", + " plt.show()\n", + " else:\n", + " print('No numeric KPI columns in the flattened table.')\n", + "else:\n", + " print('No KPI data returned — try widening time_selection or check asset name.')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Filtering with `where`\n", + "`get_kpi_data_viz` also accepts a `where` list to filter the underlying records, same shape as MA's Data Viz filter." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Points returned with filter: 103\n" + ] + } + ], + "source": [ + "# Example filter — adapt field name / value to your tenant\n", + "# where_args = {'where': [{'name': 'oee', 'op': 'gte', 'value': 50}]}\n", + "where_args = {'where': []}\n", + "\n", + "filtered = cli.get_kpi_data_viz(\n", + " machine_sources=this_machine_sources,\n", + " kpis=this_kpis,\n", + " i_vars=this_i_vars,\n", + " time_selection=this_time_selection,\n", + " **where_args,\n", + ")\n", + "print(f'Points returned with filter: {len(filtered)}')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "ds-team", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/3-cookbooks.ipynb b/examples/3-cookbooks.ipynb new file mode 100644 index 0000000..2ec2ee6 --- /dev/null +++ b/examples/3-cookbooks.ipynb @@ -0,0 +1,1157 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# smsdk Example 3 — Cookbooks\n", + "\n", + "Listing cookbooks, inspecting their structure, pulling top runs for a recipe group, and reading current tag values.\n", + "\n", + "For full method signatures and parameter reference, see [docs/README.md](../docs/README.md#cookbooks).\n", + "\n", + "**Updated:** April 2026 — replaces the former `Cookbooks Examples.ipynb`.\n", + "\n", + "> **Terminology:** what the UI calls **Products** are represented in the SDK as **recipe groups**." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from smsdk import client\n", + "import pandas as pd\n", + "import os" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Set before running this notebook.\n", + "tenant = \"demo-bottling\"\n", + "api_key = \"\"\n", + "api_secret = \"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "cli = client.Client(tenant)\n", + "success = cli.login('apikey', key_id=api_key, secret_id=api_secret)\n", + "assert success, 'SDK login failed — check tenant / API key / secret.'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## List cookbooks\n", + "\n", + "`get_cookbooks()` returns every cookbook on the tenant — both deployed and undeployed. Each cookbook is a nested dict; the top-level DataFrame below just surfaces name / assets / id." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total cookbooks on this tenant: 21\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
nameassetNamesid
0L1: Performance and Quality[Blender_1, Packer_2, Packer_4, Filler_1, L1_W...66980b5f337841084825c5b9
1Line One Efficiency[L1_FullCan_Conveyor, Blender_1, Palletizer_2,...669aab34dc43a850b5b995ec
2DEMO - L1: Performance and Quality[Blender_1, Packer_2, Packer_4, Filler_1, L1_W...675313e872adb6a058ba16b3
3L1 - Filler #2 (1B): Essence[Filler_2, Filler_1, Blender_1, L1_Warmer, Fil...67e1be02956f86513155e297
4L1 - Empty Can Conveyor: VFD_102_Fault_2_desc[Empty_Can_Conveyor, Filler_1, Filtecs_Combine...68dd822be4f770ee248500e2
5DELETE L1 - Filler #1 (1A): Expected CIP Type[Filler_1]68e40c70477426a04ff5df3e
6Packer : CIP[Packer_2, Packer_3, Packer_4]68f639b4e7d03b83685af115
7Filler : CIP[Filler_2, Filler_1]68f92190fd233d005af6849f
8Filler : 5000 Cases Threshold[Filler_1, Filler_2]68f930e4a81c2e550ed616f0
9Filler : 3000 Cases Threshold[Filler_1, Filler_2]69206ba9dc54668f74640532
10L1 - Filler #1 (1A): CIP[Filler_1]697e7b20e2973c6150a91777
11Copy of Packer : CIP[Packer_2, Packer_3, Packer_4]69813b551f886bfed5f9c4ce
12Copy of Copy of Packer : CIP[Packer_2, Packer_3, Packer_4]6982bfa7bf311418837e9173
13Copy of Packer : CIP[Packer_2, Packer_3, Packer_4]6982c0210faf5efcc96ba784
14Copy of Copy of Packer : CIP[Packer_2, Packer_3, Packer_4]6982c0300f4779a9e1788ff6
15Copy of Copy of Copy of Packer : CIP[Packer_2, Packer_3, Packer_4]6983800e5ea4ab244e7ec1c0
16Copy of Copy of Packer : CIP[Packer_2, Packer_3, Packer_4]69838013cd55bbeb20afdde0
17Copy of Copy of Packer : CIP[Packer_2, Packer_3, Packer_4]69839be77335b99046af4b5d
18Copy of L1 - Filler #1 (1A): CIP[Filler_1]69839cc5665fab9eef7938a9
19L1 - Palletizer #2: Machine Fault Reasons[Palletizer_1, Filtecs_Combined, Empty_Can_Con...698628f9a6935ccd021e36b9
20L1 - Packer #3 (MEAD 1B): Essence[Packer_3]698c67506f659489b88d589c
\n", + "
" + ], + "text/plain": [ + " name \\\n", + "0 L1: Performance and Quality \n", + "1 Line One Efficiency \n", + "2 DEMO - L1: Performance and Quality \n", + "3 L1 - Filler #2 (1B): Essence \n", + "4 L1 - Empty Can Conveyor: VFD_102_Fault_2_desc \n", + "5 DELETE L1 - Filler #1 (1A): Expected CIP Type \n", + "6 Packer : CIP \n", + "7 Filler : CIP \n", + "8 Filler : 5000 Cases Threshold \n", + "9 Filler : 3000 Cases Threshold \n", + "10 L1 - Filler #1 (1A): CIP \n", + "11 Copy of Packer : CIP \n", + "12 Copy of Copy of Packer : CIP \n", + "13 Copy of Packer : CIP \n", + "14 Copy of Copy of Packer : CIP \n", + "15 Copy of Copy of Copy of Packer : CIP \n", + "16 Copy of Copy of Packer : CIP \n", + "17 Copy of Copy of Packer : CIP \n", + "18 Copy of L1 - Filler #1 (1A): CIP \n", + "19 L1 - Palletizer #2: Machine Fault Reasons \n", + "20 L1 - Packer #3 (MEAD 1B): Essence \n", + "\n", + " assetNames \\\n", + "0 [Blender_1, Packer_2, Packer_4, Filler_1, L1_W... \n", + "1 [L1_FullCan_Conveyor, Blender_1, Palletizer_2,... \n", + "2 [Blender_1, Packer_2, Packer_4, Filler_1, L1_W... \n", + "3 [Filler_2, Filler_1, Blender_1, L1_Warmer, Fil... \n", + "4 [Empty_Can_Conveyor, Filler_1, Filtecs_Combine... \n", + "5 [Filler_1] \n", + "6 [Packer_2, Packer_3, Packer_4] \n", + "7 [Filler_2, Filler_1] \n", + "8 [Filler_1, Filler_2] \n", + "9 [Filler_1, Filler_2] \n", + "10 [Filler_1] \n", + "11 [Packer_2, Packer_3, Packer_4] \n", + "12 [Packer_2, Packer_3, Packer_4] \n", + "13 [Packer_2, Packer_3, Packer_4] \n", + "14 [Packer_2, Packer_3, Packer_4] \n", + "15 [Packer_2, Packer_3, Packer_4] \n", + "16 [Packer_2, Packer_3, Packer_4] \n", + "17 [Packer_2, Packer_3, Packer_4] \n", + "18 [Filler_1] \n", + "19 [Palletizer_1, Filtecs_Combined, Empty_Can_Con... \n", + "20 [Packer_3] \n", + "\n", + " id \n", + "0 66980b5f337841084825c5b9 \n", + "1 669aab34dc43a850b5b995ec \n", + "2 675313e872adb6a058ba16b3 \n", + "3 67e1be02956f86513155e297 \n", + "4 68dd822be4f770ee248500e2 \n", + "5 68e40c70477426a04ff5df3e \n", + "6 68f639b4e7d03b83685af115 \n", + "7 68f92190fd233d005af6849f \n", + "8 68f930e4a81c2e550ed616f0 \n", + "9 69206ba9dc54668f74640532 \n", + "10 697e7b20e2973c6150a91777 \n", + "11 69813b551f886bfed5f9c4ce \n", + "12 6982bfa7bf311418837e9173 \n", + "13 6982c0210faf5efcc96ba784 \n", + "14 6982c0300f4779a9e1788ff6 \n", + "15 6983800e5ea4ab244e7ec1c0 \n", + "16 69838013cd55bbeb20afdde0 \n", + "17 69839be77335b99046af4b5d \n", + "18 69839cc5665fab9eef7938a9 \n", + "19 698628f9a6935ccd021e36b9 \n", + "20 698c67506f659489b88d589c " + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cookbooks = cli.get_cookbooks()\n", + "print(f'Total cookbooks on this tenant: {len(cookbooks)}')\n", + "\n", + "df_cookbooks = pd.DataFrame(cookbooks)\n", + "df_cookbooks[['name', 'assetNames', 'id']]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Settings — pick a cookbook and recipe group (edit me)\n", + "Adjust these indices to point at a cookbook + product that exist on your tenant. All subsequent cells use these values." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Cookbook: L1: Performance and Quality\n", + "Product: ['Cola']\n", + "Assets: ['Blender_1', 'Packer_2', 'Packer_4', 'Filler_1', 'L1_Warmer', 'Packer_3', 'Filtecs_Combined', 'Filler_2']\n" + ] + } + ], + "source": [ + "cookbook_idx = 0 # which cookbook to explore\n", + "recipe_group_idx = 0 # which product within that cookbook\n", + "\n", + "cookbook = cookbooks[cookbook_idx]\n", + "recipe_group = cookbook['recipe_groups'][recipe_group_idx]\n", + "\n", + "print(f\"Cookbook: {cookbook['name']}\")\n", + "print(f\"Product: {recipe_group.get('values')}\")\n", + "print(f\"Assets: {cookbook.get('assetNames')}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Inspect the cookbook structure\n", + "\n", + "Each cookbook has one or more **recipe groups** (products). Each recipe group has **outcomes** (what you're optimizing), **levers** (the knobs you can turn), and **constraints** (conditions that partition the data)." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Outcomes:\n", + " stats__CPM__val (weight: 1)\n", + " availability (weight: 1)\n", + "\n", + "Levers:\n", + " stats__HMI_Auto_Man_Speed_Sel__val\n", + " stats__HMI_Auto_Man_PBLT__val\n", + " stats__HMI_Infeed_Primed_PL__val\n", + " stats__AcDriveInput__val\n", + " stats__B254[19]__val\n", + " stats__Filler_No1_1A_SPEED___val\n", + " stats__Filler_No1_Infeed_Counter__val\n", + " stats__Filler_No1_Discharge_Counter__val\n", + " stats__GPS_1125OUT4__val\n", + " stats__Low_Gas_Purge_PB_LT_Bit__val\n", + " stats__Low_Gas_Purge_PB_Light__val\n", + "\n", + "Constraints (Conditions):\n" + ] + } + ], + "source": [ + "print('Outcomes:')\n", + "for o in recipe_group['outcomes']:\n", + " print(f\" {o['field']['fieldName']} (weight: {o['weight']})\")\n", + "\n", + "print('\\nLevers:')\n", + "for lever in recipe_group['levers']:\n", + " print(f\" {lever['fieldName']}\")\n", + "\n", + "print('\\nConstraints (Conditions):')\n", + "for c in recipe_group['constraints']:\n", + " print(f\" {c['field']['fieldName']}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[('HkPPotruC', ['Cola']),\n", + " ('HyxPwjtHuR', ['Cola Zero Sugar']),\n", + " ('rJbPPiFB_0', [' V Coke']),\n", + " ('HyzDwsFBd0', ['STRAWBERRY']),\n", + " ('HymPPjYBuA', ['Pepper Zero Sugar']),\n", + " ('SyNPvsKHO0', ['Cola Zero SUGAR']),\n", + " ('S1SDPsYrdA', ['Ginger Ale']),\n", + " ('ryUvPjYrOA', ['ROOT BEER']),\n", + " ('BJwvvjKB_C', ['Spiced']),\n", + " ('S1dPDsYH_C', ['Lemon Lime NEW']),\n", + " ('SkKwPoYBOA', ['Dr Easy']),\n", + " ('SJcwPjtSuC', ['Cola ZERO']),\n", + " ('S1swvsYBuC', ['Vintage']),\n", + " ('BynwvsFBu0', ['Diet Cola']),\n", + " ('SJpDvitBu0', ['PEPPER']),\n", + " ('BJAvDiYB_A', ['ORANGE']),\n", + " ('By1ewPoFrdA', ['Zero Sugar Spiced']),\n", + " ('H1xxvwjtHdC', ['Lemon Lime Zero']),\n", + " ('Sy-lwPjFSOC', ['COLA ZERO']),\n", + " ('ByMePPoYHd0', ['DIET Cola'])]" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# All recipe-group IDs within the selected cookbook\n", + "recipe_group_ids = [(rg['id'], rg.get('values')) for rg in cookbook['recipe_groups']]\n", + "recipe_group_ids" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Top runs for a recipe group\n", + "\n", + "`get_cookbook_top_results(recipe_group_id, limit)` returns the top runs for a product, sorted according to the cookbook's outcome weights. Two views come back:\n", + "- **`runs`** — one row per run (matches the UI's Runs view)\n", + "- **`constraint_groups`** — one row per recipe (matches the UI's Recipes view)\n", + "\n", + "> The cookbook must be **deployed**; an undeployed cookbook returned by `get_cookbooks()` will error here." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Runs returned: 10\n", + "Recipes returned: 1\n" + ] + } + ], + "source": [ + "results = cli.get_cookbook_top_results(recipe_group['id'], limit=10)\n", + "runs = results['runs']\n", + "recipes = results['constraint_groups']\n", + "\n", + "print(f'Runs returned: {len(runs)}')\n", + "print(f'Recipes returned: {len(recipes)}')" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'_count': 3,\n", + " '_count_muted': 0,\n", + " '_duration_seconds': 80.0,\n", + " '_earliest': '2025-09-08T05:11:40+00:00',\n", + " '_latest': '2025-09-08T05:13:00+00:00',\n", + " '_score': 0.9216773792245491,\n", + " 'constraint_group_id': '0',\n", + " 'constraints': [],\n", + " 'cookbook': '66980b5f337841084825c5b9',\n", + " 'i_vals': [{'name': 'group', 'asset': 'SHARED', 'value': '0'},\n", + " {'name': 'sequence', 'asset': 'SHARED', 'value': 17}],\n", + " 'filters': [],\n", + " 'levers': [{'name': 'stats__HMI_Auto_Man_Speed_Sel__val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 3,\n", + " 'value': {'min': 1.0,\n", + " 'max': 1.0,\n", + " 'avg': 1.0,\n", + " 'var_pop': 0.0,\n", + " 'count': 2.0}},\n", + " {'name': 'stats__HMI_Auto_Man_PBLT__val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 4,\n", + " 'value': {'min': 1.0,\n", + " 'max': 1.0,\n", + " 'avg': 1.0,\n", + " 'var_pop': 0.0,\n", + " 'count': 2.0}},\n", + " {'name': 'stats__HMI_Infeed_Primed_PL__val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 5,\n", + " 'value': {'min': 1.0,\n", + " 'max': 1.0,\n", + " 'avg': 1.0,\n", + " 'var_pop': 0.0,\n", + " 'count': 2.0}},\n", + " {'name': 'stats__AcDriveInput__val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 6,\n", + " 'value': {'min': 25569.285714285714,\n", + " 'max': 27184.0,\n", + " 'avg': 26376.642857142855,\n", + " 'var_pop': 651825.5561224464,\n", + " 'count': 2.0}},\n", + " {'name': 'stats__B254[19]__val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 7,\n", + " 'value': {'min': None,\n", + " 'max': None,\n", + " 'avg': None,\n", + " 'var_pop': None,\n", + " 'count': 0.0}},\n", + " {'name': 'stats__Filler_No1_1A_SPEED___val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 8,\n", + " 'value': {'min': 669.0,\n", + " 'max': 1108.0,\n", + " 'avg': 888.5,\n", + " 'var_pop': 48180.25,\n", + " 'count': 2.0}},\n", + " {'name': 'stats__Filler_No1_Infeed_Counter__val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 9,\n", + " 'value': {'min': 744761.4285714285,\n", + " 'max': 759750.0,\n", + " 'avg': 752255.7142857143,\n", + " 'var_pop': 56164318.36734819,\n", + " 'count': 2.0}},\n", + " {'name': 'stats__Filler_No1_Discharge_Counter__val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 10,\n", + " 'value': {'min': 744761.4285714285,\n", + " 'max': 759750.0,\n", + " 'avg': 752255.7142857143,\n", + " 'var_pop': 56164318.36734819,\n", + " 'count': 2.0}},\n", + " {'name': 'stats__GPS_1125OUT4__val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 11,\n", + " 'value': {'min': 0.25,\n", + " 'max': 0.7142857142857143,\n", + " 'avg': 0.48214285714285715,\n", + " 'var_pop': 0.05389030612244898,\n", + " 'count': 2.0}},\n", + " {'name': 'stats__Low_Gas_Purge_PB_LT_Bit__val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 12,\n", + " 'value': {'min': 0.2857142857142857,\n", + " 'max': 0.75,\n", + " 'avg': 0.5178571428571428,\n", + " 'var_pop': 0.05389030612244896,\n", + " 'count': 2.0}},\n", + " {'name': 'stats__Low_Gas_Purge_PB_Light__val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 13,\n", + " 'value': {'min': 0.2857142857142857,\n", + " 'max': 0.75,\n", + " 'avg': 0.5178571428571428,\n", + " 'var_pop': 0.05389030612244896,\n", + " 'count': 2.0}}],\n", + " 'outcomes': [{'name': 'stats__CPM__val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 0,\n", + " 'value': {'min': 1093.4285714285713,\n", + " 'max': 1285.0,\n", + " 'avg': 1162.142857142857,\n", + " 'var_pop': 7582.326530612259,\n", + " 'count': 3.0,\n", + " 'normal': 0.6867095168981961},\n", + " 'type': 'continuous'},\n", + " {'name': 'availability',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 1,\n", + " 'value': {'min': 100.0,\n", + " 'max': 100.0,\n", + " 'avg': 100.0,\n", + " 'var_pop': 100.0,\n", + " 'count': 100.0,\n", + " 'normal': 1.0},\n", + " 'kpi': {'dependencies': {'up_time': 177000.0, 'all_time': 177000.0},\n", + " 'formula': '(up_time/all_time)*100',\n", + " 'aggregates': {'up_time': 'sum', 'all_time': 'sum'}},\n", + " 'type': 'kpi'}]}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Sample run — inspect structure\n", + "runs[0] if runs else 'No runs'" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'_score': 0.8436440625156457,\n", + " '_count': 900,\n", + " '_count_muted': 0,\n", + " '_run_count': 10,\n", + " '_duration_seconds': 53062.0,\n", + " '_earliest': '2024-05-08T04:44:26+00:00',\n", + " '_latest': '2025-11-06T09:32:00+00:00',\n", + " 'constraint_group_id': '0',\n", + " 'constraints': [],\n", + " 'filters': [],\n", + " 'levers': [{'name': 'stats__HMI_Auto_Man_Speed_Sel__val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 3,\n", + " 'value': {'avg': 0.8957169609416799,\n", + " 'normal': None,\n", + " 'std': 0.30204612131804326,\n", + " 'max': 1.0,\n", + " 'min': 0.0}},\n", + " {'name': 'stats__HMI_Auto_Man_PBLT__val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 4,\n", + " 'value': {'avg': 0.8957169609416799,\n", + " 'normal': None,\n", + " 'std': 0.30204612131804326,\n", + " 'max': 1.0,\n", + " 'min': 0.0}},\n", + " {'name': 'stats__HMI_Infeed_Primed_PL__val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 5,\n", + " 'value': {'avg': 0.8727314071696094,\n", + " 'normal': None,\n", + " 'std': 0.30607699845632763,\n", + " 'max': 1.0,\n", + " 'min': 0.0}},\n", + " {'name': 'stats__AcDriveInput__val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 6,\n", + " 'value': {'avg': 24486.194197431785,\n", + " 'normal': None,\n", + " 'std': 5486.651037388305,\n", + " 'max': 28679.0,\n", + " 'min': 13505.0}},\n", + " {'name': 'stats__B254[19]__val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 7,\n", + " 'value': {'avg': None,\n", + " 'normal': None,\n", + " 'std': None,\n", + " 'max': None,\n", + " 'min': None}},\n", + " {'name': 'stats__Filler_No1_1A_SPEED___val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 8,\n", + " 'value': {'avg': 977.2023434991975,\n", + " 'normal': None,\n", + " 'std': 540.6868872069019,\n", + " 'max': 1378.0,\n", + " 'min': 0.0}},\n", + " {'name': 'stats__Filler_No1_Infeed_Counter__val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 9,\n", + " 'value': {'avg': 866254.3730069558,\n", + " 'normal': None,\n", + " 'std': 89826.46240460608,\n", + " 'max': 1014630.0,\n", + " 'min': 738572.0}},\n", + " {'name': 'stats__Filler_No1_Discharge_Counter__val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 10,\n", + " 'value': {'avg': 866254.3730069558,\n", + " 'normal': None,\n", + " 'std': 89826.46240460608,\n", + " 'max': 1014630.0,\n", + " 'min': 738572.0}},\n", + " {'name': 'stats__GPS_1125OUT4__val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 11,\n", + " 'value': {'avg': 0.012413055109684323,\n", + " 'normal': None,\n", + " 'std': 0.09092575918108085,\n", + " 'max': 1.0,\n", + " 'min': 0.0}},\n", + " {'name': 'stats__Low_Gas_Purge_PB_LT_Bit__val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 12,\n", + " 'value': {'avg': 0.9875869448903154,\n", + " 'normal': None,\n", + " 'std': 0.09092575918108084,\n", + " 'max': 1.0,\n", + " 'min': 0.0}},\n", + " {'name': 'stats__Low_Gas_Purge_PB_Light__val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 13,\n", + " 'value': {'avg': 0.9875869448903154,\n", + " 'normal': None,\n", + " 'std': 0.09092575918108084,\n", + " 'max': 1.0,\n", + " 'min': 0.0}}],\n", + " 'outcomes': [{'name': 'stats__CPM__val',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 0,\n", + " 'value': {'avg': 982.4581746031745,\n", + " 'normal': 0.47633542090674447,\n", + " 'std': 546.9055139344927,\n", + " 'max': 1378.0,\n", + " 'min': 0.0},\n", + " 'type': 'continuous'},\n", + " {'name': 'availability',\n", + " 'asset': 'Filler_1',\n", + " 'd_pos': 1,\n", + " 'value': {'avg': 86.05676597006077,\n", + " 'normal': 0.8982408291558388,\n", + " 'std': None,\n", + " 'max': 100.0,\n", + " 'min': 85.43263964950711,\n", + " 'kpi_dependencies': {'up_time': 46450000.0, 'all_time': 53976000.0}},\n", + " 'type': 'kpi'}],\n", + " 'cookbook': '66980b5f337841084825c5b9'}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Sample recipe (constraint group) — inspect structure\n", + "recipes[0] if recipes else 'No recipes'" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total runs: 10\n", + "With unmuted data: 10\n" + ] + } + ], + "source": [ + "# Quick stats: how many runs have unmuted records?\n", + "if runs:\n", + " unmuted = [r for r in runs if r['_count'] > r['_count_muted']]\n", + " print(f'Total runs: {len(runs)}')\n", + " print(f'With unmuted data: {len(unmuted)}')" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
_count_count_muted_duration_seconds_earliest_latest_scoreconstraint_group_idconstraintscookbooki_valsfiltersleversoutcomes
03080.02025-09-08T05:11:40+00:002025-09-08T05:13:00+00:000.9216770[]66980b5f337841084825c5b9[{'name': 'group', 'asset': 'SHARED', 'value':...[][{'name': 'stats__HMI_Auto_Man_Speed_Sel__val'...[{'name': 'stats__CPM__val', 'asset': 'Filler_...
13073.02025-09-08T09:00:47+00:002025-09-08T09:02:00+00:000.9001770[]66980b5f337841084825c5b9[{'name': 'group', 'asset': 'SHARED', 'value':...[][{'name': 'stats__HMI_Auto_Man_Speed_Sel__val'...[{'name': 'stats__CPM__val', 'asset': 'Filler_...
270325.02025-09-08T04:44:35+00:002025-09-08T04:50:00+00:000.8956460[]66980b5f337841084825c5b9[{'name': 'group', 'asset': 'SHARED', 'value':...[][{'name': 'stats__HMI_Auto_Man_Speed_Sel__val'...[{'name': 'stats__CPM__val', 'asset': 'Filler_...
370321.02025-09-08T07:39:39+00:002025-09-08T07:45:00+00:000.8859460[]66980b5f337841084825c5b9[{'name': 'group', 'asset': 'SHARED', 'value':...[][{'name': 'stats__HMI_Auto_Man_Speed_Sel__val'...[{'name': 'stats__CPM__val', 'asset': 'Filler_...
440152.02025-09-08T07:52:28+00:002025-09-08T07:55:00+00:000.8425830[]66980b5f337841084825c5b9[{'name': 'group', 'asset': 'SHARED', 'value':...[][{'name': 'stats__HMI_Auto_Man_Speed_Sel__val'...[{'name': 'stats__CPM__val', 'asset': 'Filler_...
\n", + "
" + ], + "text/plain": [ + " _count _count_muted _duration_seconds _earliest \\\n", + "0 3 0 80.0 2025-09-08T05:11:40+00:00 \n", + "1 3 0 73.0 2025-09-08T09:00:47+00:00 \n", + "2 7 0 325.0 2025-09-08T04:44:35+00:00 \n", + "3 7 0 321.0 2025-09-08T07:39:39+00:00 \n", + "4 4 0 152.0 2025-09-08T07:52:28+00:00 \n", + "\n", + " _latest _score constraint_group_id constraints \\\n", + "0 2025-09-08T05:13:00+00:00 0.921677 0 [] \n", + "1 2025-09-08T09:02:00+00:00 0.900177 0 [] \n", + "2 2025-09-08T04:50:00+00:00 0.895646 0 [] \n", + "3 2025-09-08T07:45:00+00:00 0.885946 0 [] \n", + "4 2025-09-08T07:55:00+00:00 0.842583 0 [] \n", + "\n", + " cookbook \\\n", + "0 66980b5f337841084825c5b9 \n", + "1 66980b5f337841084825c5b9 \n", + "2 66980b5f337841084825c5b9 \n", + "3 66980b5f337841084825c5b9 \n", + "4 66980b5f337841084825c5b9 \n", + "\n", + " i_vals filters \\\n", + "0 [{'name': 'group', 'asset': 'SHARED', 'value':... [] \n", + "1 [{'name': 'group', 'asset': 'SHARED', 'value':... [] \n", + "2 [{'name': 'group', 'asset': 'SHARED', 'value':... [] \n", + "3 [{'name': 'group', 'asset': 'SHARED', 'value':... [] \n", + "4 [{'name': 'group', 'asset': 'SHARED', 'value':... [] \n", + "\n", + " levers \\\n", + "0 [{'name': 'stats__HMI_Auto_Man_Speed_Sel__val'... \n", + "1 [{'name': 'stats__HMI_Auto_Man_Speed_Sel__val'... \n", + "2 [{'name': 'stats__HMI_Auto_Man_Speed_Sel__val'... \n", + "3 [{'name': 'stats__HMI_Auto_Man_Speed_Sel__val'... \n", + "4 [{'name': 'stats__HMI_Auto_Man_Speed_Sel__val'... \n", + "\n", + " outcomes \n", + "0 [{'name': 'stats__CPM__val', 'asset': 'Filler_... \n", + "1 [{'name': 'stats__CPM__val', 'asset': 'Filler_... \n", + "2 [{'name': 'stats__CPM__val', 'asset': 'Filler_... \n", + "3 [{'name': 'stats__CPM__val', 'asset': 'Filler_... \n", + "4 [{'name': 'stats__CPM__val', 'asset': 'Filler_... " + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Flat runs table\n", + "df_runs = pd.DataFrame(runs)\n", + "df_runs.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
_score_count_count_muted_run_count_duration_seconds_earliest_latestconstraint_group_idconstraintsfiltersleversoutcomescookbook
00.84364490001053062.02024-05-08T04:44:26+00:002025-11-06T09:32:00+00:000[][][{'name': 'stats__HMI_Auto_Man_Speed_Sel__val'...[{'name': 'stats__CPM__val', 'asset': 'Filler_...66980b5f337841084825c5b9
\n", + "
" + ], + "text/plain": [ + " _score _count _count_muted _run_count _duration_seconds \\\n", + "0 0.843644 900 0 10 53062.0 \n", + "\n", + " _earliest _latest constraint_group_id \\\n", + "0 2024-05-08T04:44:26+00:00 2025-11-06T09:32:00+00:00 0 \n", + "\n", + " constraints filters levers \\\n", + "0 [] [] [{'name': 'stats__HMI_Auto_Man_Speed_Sel__val'... \n", + "\n", + " outcomes cookbook \n", + "0 [{'name': 'stats__CPM__val', 'asset': 'Filler_... 66980b5f337841084825c5b9 " + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Flat recipes table (matches the Recipes view in the UI)\n", + "df_recipes = pd.DataFrame(recipes)\n", + "df_recipes.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Normalize range-based constraints to strings\n", + "\n", + "`normalize_constraints` turns a list of range-constraint dicts (each with `to` / `from` / inclusivity flags) into compact string labels like `[120,None)`. Useful for printing recipes. Only works on continuous constraints — skip this for categorical ones." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "if runs and runs[0].get('constraints'):\n", + " ranges = [c['values'] for c in runs[0]['constraints']]\n", + " print('Raw:')\n", + " for r in ranges:\n", + " print(f' {r}')\n", + " print('\\nNormalized:')\n", + " for s in cli.normalize_constraints(ranges):\n", + " print(f' {s}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Current tag values (`get_cookbook_current_value`)\n", + "\n", + "`get_cookbook_current_value(variables, minutes)` returns the most recent value of one or more tags. Useful for comparing live process state to the top-run recipes above.\n", + "\n", + "- **`variables`**: list of `{'asset': machine_name, 'name': field_name}` dicts. Both must be the **system names**, not display names — copy them off a run returned by `get_cookbook_top_results` if in doubt.\n", + "- **`minutes`**: lookback window (default 1440 = 1 day). Returns `None` for any tag with no readings in that window.\n", + "- One invalid entry will error the whole call without telling you which one." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "No runs/constraints available to derive an example from — edit the cell to hard-code asset/name.\n" + ] + } + ], + "source": [ + "# Pull asset + tag names off the first run if available; otherwise edit these by hand.\n", + "if runs and runs[0].get('constraints'):\n", + " example_constraint = runs[0]['constraints'][0]\n", + " vars_to_check = [{\n", + " 'asset': runs[0].get('asset'),\n", + " 'name': example_constraint['field']['fieldName'],\n", + " }]\n", + " print('Querying:', vars_to_check)\n", + " vals = cli.get_cookbook_current_value(vars_to_check)\n", + " display(pd.DataFrame(vals))\n", + "else:\n", + " print('No runs/constraints available to derive an example from — edit the cell to hard-code asset/name.')" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "# Example: short lookback returns None if no data in the window\n", + "if runs and runs[0].get('constraints'):\n", + " vals = cli.get_cookbook_current_value(vars_to_check, minutes=0.5)\n", + " pd.DataFrame(vals)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "ds-team", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}