Jump to content

Leaderboard

Popular Content

Showing content with the highest reputation since 01/25/2024 in all areas

  1. There are instances where it's desirable to add or remove Access Control to a user created datasource. For instance, you may want to push data from Seeq Datalab to a Datasource that is only available to a sub-set of users. Below is a tip how to add additional entries to a Datasource Access Control List (ACL) using `spy.acl`. 1) Identify Datasource ID If not already known, retrieve the ID for the Datasource. This can be done via the Seeq REST API (accessible via the "hamburger" menu in the upper right corner of your browser window). This is different from the datasourceId identifier and will contain a series of characters as shown below. It can be retrieved via the `GET/datasources` endpoint under the `Datasources` section. The response body will contain the ID: 2) Identify User and/or Group ID We will also want to identify the User and/or Group ID. Below is an example of how to retrieve a Group ID via the `UserGroups` GET/usergroups endpoint: The response body will contain the ID: 3) Use `spy.acl()` to add group to the Datasource Access Control List (ACL) Launch a Seeq Datalab Project and create a new ipynb Notebook Assign the two IDs to a variable datasource_id='65BDFD4F-1FA3-4EB5-B21E-069FA5A772DF' group_id='19F1B7DC-69BE-4402-AD3B-DF89B1B9A1A4' (Optional) - Check the current Access Control List for the Datasource using `spy.acl.pull()` current_access_control_df=spy.acl.pull(datasource_id) current_access_control_df.iloc[0]['Access Control'] Add the group_id to the Datasource ACL using `spy.acl.push()` spy.acl.push(datasource_id, { 'ID': group_id, 'Read': True, 'Write': True, 'Manage': False }, replace=False) Note I've set `replace=False`, which means the group will be appended to the current access list. If the desire is to the replace the entire existing ACL list, this can be toggled to `replace=True`. Similarly, you can adjust the read/write/manage permissions to fit your need. For more information on `spy.acl.push`, reference the `spy.acl.ipynb` document located in the SPy Documentation folder or access the docstring by executing help(spy.acl.push) (Optional) - To confirm the correct ID has been added, re-pull the ACL via `spy.acl.pull()` current_access_control_df=spy.acl.pull(datasource_id) current_access_control_df.iloc[0]['Access Control']
    2 points
  2. Hello Bill, if you do not have Data Lab than you can use the REST API of Seeq. I created an example using Powershell: $baseurl = "https://myseeqinstance" $headers = @{ "accept" = "application/vnd.seeq.v1+json" "Content-Type" = "application/vnd.seeq.v1+json" } $body = @{ "authProviderClass" = "Auth" "authProviderId" = "Seeq" "code" = $null "password" = "<password>" "state" = $null "username" = "<username>" } | ConvertTo-Json $url = $baseurl + "/api/auth/login" $response = Invoke-WebRequest -Method Post -Uri $url -Headers $headers -body $body $headers = @{ "accept" = "application/vnd.seeq.v1+json" "x-sq-auth" = $response.Headers.'x-sq-auth' } $url = $baseurl + "/api/users?sortOrder=email%20asc&offset=0&limit=200" $response = Invoke-RestMethod -Method Get -Uri $url -Headers $headers $selectedAttributes = $response.users | Select-Object -Property firstname, lastname, email $selectedAttributes | Export-Csv -Path "userlist.csv" -NoTypeInformation The script uses the /auth/login endpoint to authenticate the user and retrieves the list of users (limited to 200) using the /users endpoint. You would need to replace the value in the first line with the URL of your Seeq system (eg. https://mycompany.seeq.site) and specify the credentials by replacing <password> and <username> inside the script. The list of users will be written to the file userlist.csv inside the directory from which the powershell script is run. Let me know if this works for you. Regards, Thorsten
    1 point
  3. Working with Asset Groups or spy.assets.Tree() has been effective. However, as your project expands, the process of adding and managing additional assets has become cumbersome. It's time to elevate your approach by transitioning to an Asset Template for building your Asset Tree. In this guide, I'll share valuable tips and tricks for creating an asset tree from an existing hierarchy. If you already have an established asset tree from another source, such as PI AF, you can leverage it as a quick starting point to build your tree in Seeq. Whether you're starting with an existing asset tree or starting from scratch, these insights will help you seamlessly integrate your asset structure into Seeq Data Lab. Additionally, I'll cover essential troubleshooting techniques, focusing on maximizing ease of use when using Asset Templates. In a future topic, I'll discuss the process of building an Asset Tree from scratch using Asset Templates. Let's dive into the best practices for optimizing your asset management workflow with Seeq Data Lab. Why Templates over Trees? The choice between using Asset Templates and spy.assets.Tree() depends on the complexity of your project and the level of customization you need. Here are some reasons why you might want to use Asset Templates instead of spy.assets.Tree(): Object-Oriented Design: Asset Templates allow you to leverage object-oriented design principles like encapsulation, inheritance, composition, and mixins. This can increase code reuse and consistency, especially in larger projects. However, the learning curve is a bit longer if you do not come from a programming background. Customization: Asset Templates provide a higher level of customization. You can define your own classes and methods, which can be useful if you need to implement complex logic or calculations. You can more easily create complex hierarchical trees similar Asset Trees from PI AF. Handling Exceptions: Manufacturing scenarios often have many exceptions. Asset Templates can accommodate these exceptions more easily than spy.assets.Tree(). Here are some reasons why you might want to use spy.assets.Tree() instead of Asset Templates: Simplicity: spy.assets.Tree() is simpler to use and understand, especially for beginners or for simple projects. If you just need to create a basic hierarchical structure without any custom attributes or methods, spy.assets.Tree() is a good choice. You can always graduate to Asset Templates later. Flat Structure: If your data is already organized in a flat structure (like a CSV file), spy.assets.Tree() can easily convert that structure into an asset tree. Less Code: With spy.assets.Tree(), you can create an asset tree with just a few lines of code. You don't need to define any classes or methods. Okay, we are settled on Asset Templates. What's first? If you have an existing asset hierarchy with the attributes you want to use in your calculations, you can start there. I'll be using the Example >> Cooling Tower 1 as my example existing Asset Tree. We'll use the asset tree path for our spy.search criteria and use the existing asset hierarchy when building our Template. The attachment includes a complete working Jupyter notebook. You can upload it to a Data Lab project and test each step as described here. Okay, let's review the code section by section. # 1. Importing Libraries from seeq import spy import pandas as pd import re from seeq.spy.assets import Asset # 2. Checking and displaying the spy version version = spy.__version__ print(version) # 3. Setting the compatibility option so that you maximize the chance that SPy # will remain compatible with your notebook/script spy.options.compatibility = 189 # 4. Disply configuration for Pandas to show all columns in DataFrame output pd.set_option('display.max_colwidth', None) In this cell, the script initializes the Seeq Python (SPy) library as well as other required libraries, checks the spy version, sets a compatibility option, and configures pandas to display DataFrame output without column width truncation. # 1. Use spy.search to search for assets using the existing Asset Tree hierarchy. metadata = spy.search( {'Path': 'Example >> Cooling Tower 1 >> Area*'}, # MODIFY: Change the search path old_asset_format=False, limit=None, ) # 2. Split the "Path" column by the ">>" delimiter and create a new column "Modified_Path." metadata["Modified_Path"] = metadata['Path'] + ' >> ' + metadata['Asset'] split_columns = metadata['Modified_Path'].str.split(' >> ', expand=True) # 3. Rename the columns of the split DataFrame to represent different hierarchy levels. split_columns.columns = [f'Level_{i+1}' for i in range(split_columns.shape[1])] # 4. Concatenate the split columns with the original DataFrame to incorporate the hierarchy levels. metadata = pd.concat([metadata, split_columns], axis=1) # 5. Required for building an asset tree. This is the parent branch/name of the asset tree. metadata['Build Path'] = 'Asset Tree Descriptive Name' # MODIFY: Set the desired parent branch name # 6. The child branch of 'Build Path' and will be named after the "Level_2" column. metadata['Build Asset'] = metadata['Level_2'] # Ex: "Cooling Tower 1" This cell constructs the metadata used in our new asset tree. The spy.search on the existing hierarchy is used to retrieve the metadata for each attribute in the asset path. The existing tree path is "Example >> Cooling Tower 1 >> Area*". This will fetch all attributes under each of the Areas (e.g., Area A - K). However, it will not retrieve any attributes that may reside in "Example >> Cooling Tower 1. The complete path for each asset is the concatenation of the "Path" column and the "Asset" column. This "Modified Path" is how we will define each level of our new Asset Tree. We are splitting the Modified Path and inserting each level as a new column that will be referenced later when building the asset tree. In steps 5 and 6, we define the first two levels of the asset tree. In this example, the new path will start with "Asset Tree Descriptive Name >> Cooling Tower 1". By utilizing the 'Level_2' column in the 'Build Path,' if we had included all cooling towers in our spy.search, the new second level in the path could be 'Cooling Tower 2,' depending on the asset's location in the existing tree." Alternatively, if you prefer a flat tree, you can specify the name of "Level 2" as a string rather than referencing the metadata column. # This is the class will be called when using Spy.Asset.Build # This is the child branch of 'Build Asset' # Ex: Asset Tree Descriptive Name >> Cooling Tower 1 >> Area A # Can you also have attributes at these levels by adding @Asset.Attribute() within the class. class AssetTreeName(Asset): # MODIFY: AssetTreeName @Asset.Component() def Asset_Component(self, metadata): # MODIFY: Unit_Component return self.build_components( template=Level_4, # This the name of the child branch class metadata=metadata, column_name='Level_3' # Metadata column name of the desired asset; Ex: Area A ) # Example Roll Up Calculation # This roll up calculation will find the evaluate the temperature if each child branch (e.g., Area A, Area B, # Area C, etc.) and create a new signal of the maximum temperature of the group @Asset.Attribute() def Cooling_Tower_Max_Temperature(self, metadata): return self.Asset_Component().pick({ ## MODIFY: Unit_Component() if changed in @Asset.Component() above 'Name': 'Temperature Unique Raw Tag' # MODIFY:'Temperature Unique Raw Tag'; this is the Friendly name of the attribute created in Level_4 }).roll_up('maximum') # MODIFY: 'maximum' In this cell, we are building our first class in our Asset Template. In the context of Asset Templates, consider a class as a general contractor responsible for constructing the current level in your asset tree. It subcontracts the responsibility for the lower level to that level's class. TIn the class, you define attributes such as signals, calculated signals, conditions, scorecard metrics, etc. or components, which are the 'child' branches of this 'parent' branch. These components are built by a different class. This first class will be called when we use spy.assets.build to create the new asset tree. @Asset.Component() indicates that we are defining a method to construct a component. The method name is Asset_Component which will construct the child branch. The parameter 'template' defines the class that will construct the child branch, while 'column_name' specifies the name of each child branch. Here we are using "Level_3" which contains the Area name (e.g., Area A). @Asset.Attribute() indicates that we are defining a method that will be an attribute. More details about creating attributes will be discussed in the next class. However, the example provided is a roll up calculation which will be a signal equal to the maximum value of each of the child branches' "Temperature Unique Raw Tag" which will be defined in the next class. When using roll-up calculations, note that the method being called is the component method responsible for constructing the child branches, not the attribute residing in the child branches. The .pick() function utilizes the friendly name of the attribute to identify the attributes in the child branches. # This is the child branch of 'AssetTreeName' class class Level_4(Asset): @Asset.Attribute() # Raw Tag (Friendly Name is the same as the method name with '_' removed) # Use this method if you are highly confident there are no multiple matches in current or child branches def Temperature_Unique_Raw_Tag(self, metadata): # MODIFY: Temperature_Unique_Raw_Tag return metadata[metadata['Name'] == 'Temperature'] @Asset.Attribute() def Temperature_Possible_Duplicates_Raw_Tag(self, metadata): # MODIFY:Temperature_Possible_Duplicates_Raw_Tag # If multiple matching metadata is found, the first match is selected using .iloc[[0]]. # This also addresses duplicate attributes in child branches. # If selecting first match is not desired, improve regex search criteria # or add additional filters to select desired attribute (e.g, # metadata[metadata['Name'].str.contains(r'(?:Temperature)$') # & metadata[metadata['Description'].str.contains(r'(?:Discharge)')] # ) filtered = metadata[metadata['Name'].str.contains(r'(?:Temperature)$')] # MODIFY: r'(?:Temperature)$') return filtered.iloc[[0]] In our example, Level_4 is the lowest level in our asset tree. All our attributes related to an asset will reside here. In our example, our raw tags (i.e., no calculations) will reside in the same branch as our derived tags (i.e., calculated), which typically reference the raw tags in the formula parameter. Each attribute is defined by a method starting with @Asset.Attribute before defining the method. When not specified, the friendly name of the attribute is the same as the method name with the underscores removed. Example: The method name is Temperature_Unique_Raw_Tag, and the friendly name is "Temperature Unique Raw Tag". We reference the metadata dataframe with a search criteria to select the proper row for the attribute. There are several methods using pandas to select the correct row. Here we are using the "Name" to find the row with "Temperature" as the value. Spy will find each "Temperature" value for each asset. This method can result in an error if there are multiple matches. The second method "Temperature_Possible_Duplicates_Raw_Tag" explains how you can avoid this error by using .iloc[[0]] to select the first match found or how you can increase the complexity of your search criteria to further filter your results. # Calculated Tag (Signal) referencing an attribute in this class @Asset.Attribute() def Temperature_Calc(self, metadata): return { 'Name': 'Signal Friendly Name', # MODIFY: 'Signal Friendly Name' 'Type': 'Signal', # This is the same formula you would use in Workbench Formula 'Formula': '$signal.agileFilter(1min)', # MODIFY: "$signal.agileFilter(1min)"; 'Formula Parameters': {'$signal': self.Temperature_Unique_Raw_Tag()}, # MODIFY: '$signal' and self.Temperature_Unique_Raw_Tag() } # Calculated Tag (Signal) not previously referenced as an attribute @Asset.Attribute() def Wet_Bulb(self, metadata): filtered = metadata[metadata['Name'].str.contains(r'^Wet Bulb$')] # MODIFY: '^Wet Bulb$' return { 'Name': 'Signal Friendly Name 2', # MODIFY: 'Signal Friendly Name 2' 'Type': 'Signal', 'Formula': '$a.agileFilter(1min)', # MODIFY: '$a.agileFilter(1min)' 'Formula Parameters': {'$a': filtered.iloc[[0]]}, # MODIFY: '$a' } # Calculated Tag (Condition) @Asset.Attribute() def Temperature_Cond(self, metadata): return { 'Name': 'Condition Friendly Name', # MODIFY: 'Condition Friendly Name' 'Type': 'Condition', 'Formula': '$b > 90', # MODIFY: '$b > 90' 'Formula Parameters': {'$b': self.Temperature_Possible_Duplicates_Raw_Tag()}, # MODIFY: '$b' & self.Temperature_Possible_Duplicates_Raw_Tag() } # Scorecard Metric # See Asset Templates documentation for more examples of Scorecard Metrics @Asset.Attribute() def Temperature_Statistic_KPI(self, metadata): return { 'Type': 'Metric', 'Measured Item': self.Temperature_Unique_Raw_Tag(), # MODIFY: self.Temperature_Unique_Raw_Tag() 'Statistic': 'Average' # MODIFY: 'Average' } This is a continuation of our Level_4 class. Here we are creating derived attributes which use a formula or metric and reference other attributes in most cases. There are several different example attributes for calculated signals, conditions, and scorecard metrics. You can specify the friendly name in the "Name" parameter as an alternative method to using the name of the method. The value of the "Formula" is the exact same formula you use in Workbench. "Formula Parameters" is where you define your variables in your formula. The value of the "variable" key (i.e., $signal) is a method call to the attribute to be used in the formula (i.e., self.Temperature_Unique_Raw_Tag()). If you wish to specify a signal that has not been defined as an attribute, you can specify the value of the referenced variable using the same search criteria when specifying a new attribute. It is important to remember to change the "Type" when creating a condition or a scorecard metric, as this will cause an error if not properly specified. # Use this method if you want a different friendly name rather than the regex search criteria @Asset.Attribute() def Friendly_Name_Example(self, metadata): return self.select_metadata(metadata, name_contains = r'^Compressor Power$', friendly_name ='Power') #MODIFY: name_contains = r'Compressor Power', friendly_name = 'Power' # Use this method if you want the friendly name to be the same as regex search criteria # without the regex formatting @Asset.Attribute() def No_Friendly_Name_Example(self, metadata): return self.select_metadata(metadata, r'^Compressor Power$', None) # Example of a string attribute being passed through @Asset.Attribute() def String_Example(self, metadata): return self.select_metadata(metadata, r'Compressor Stage$', None) # Example of an attribute not found @Asset.Attribute() def No_Attribute_Exists(self, metadata): return self.select_metadata(metadata, r'Non-existing Attribute', None) # 1. Method below matches the metadata, perform attribute type detection, and applies proper formula def select_metadata(self, metadata, name_contains, friendly_name): filtered = metadata[metadata['Name'].str.contains(name_contains)] if not filtered.empty: # checks if metadata is a signal vs scalar and selects the first signal type if filtered['Type'].str.contains('signal', case=False).any() : signal_check = filtered[filtered['Type'].str.contains('signal', case=False) & ~filtered['Value Unit Of Measure'].str.contains('string', case=False)] filtered_signal = signal_check if len(signal_check) > 0 else filtered.iloc[[0]] selected_metadata = filtered_signal.iloc[[0]] else: selected_metadata = filtered.iloc[[0]] return self.determine_attribute_type(selected_metadata, name_contains, friendly_name) else: # This returns a signal with no values rather than not adding an attribute when # an attribute cannot be found. If this is not desired, simply comment out the # else statement. Having a signal is recommended to assist in asset swapping. return { 'Name' : friendly_name if friendly_name is not None else re.sub( r'\^|\(|\$|\(\?:|\)$|\)', '', name_contains), 'Type': 'Signal', 'Formula': 'Scalar.Invalid.toSignal()', } # 2. Determine if an attribute is a signal or scalar type def determine_attribute_type(self, metadata, name_contains, friendly_name): if metadata['Type'].str.contains('signal', case=False).any(): return self.generic_metadata_function(metadata, name_contains, 'Signal', friendly_name) elif metadata['Type'].str.contains('scalar', case=False).any(): return self.generic_metadata_function(metadata, name_contains, 'Scalar', friendly_name) return None # 3. Creates the metadata to create the attribute def generic_metadata_function(self, metadata, name_contains, formula_type, friendly_name): if friendly_name is None: # Regular expression pattern to remove caret (^), first "(", dollar ($), # non-capturing groups (?:), and trailing ")" pattern = r'\^|\(|\$|\(\?:|\)$|\)' friendly_name = re.sub(pattern, '', name_contains) # Metadata for signals includes strings. Required to separate strings to perform a # formula on non-string signals. Check if the signal is a "string" based on 'Value Unit Of Measure' if formula_type == 'Signal' and metadata['Value Unit Of Measure'].str.contains( 'string', case=False).any(): formula_type = 'String' # If signal, perform a calculation. If string or scalar, pass only the variable in formula. if formula_type == 'Signal': formula = '$signal.validValues()' # MODIFY: '$signal.validValues()' elif formula_type == 'String': formula = '$signal' else: formula = '$scalar' return { 'Name' : friendly_name, 'Type': formula_type, 'Formula': formula, 'Formula Parameters': ( {'$signal': metadata} if formula_type in ['Signal', 'String'] else {'$scalar': metadata}) } This is a continuation of the Level_4 class. This section is more of a trick than a tip. A common request is how to bulk apply a formula to all signals as an initial data cleaning step. This can be challenging if your existing asset tree contains signals, scalars, and/or strings with the same friendly name in the parent and/or child branches. I've seen this happen when an instrument does not exist for a particular asset. The attribute still exists in the asset tree but contains no data and is either a scalar or string. This method allows users to quickly perform the same calculations on all raw data signals. It will check for string, scalar, and signal types to determine if a calculation is required. If there are multiple matches, it will preferentially select a signal over a string or scalar. It will perform the appropriate calculation on the tag based on the type or return a blank signal if the attribute is not found. The blank signal is a necessary evil for asset swapping if the attribute is used in a calculation for other attributes. The formula should be adjusted to handle when there are no valid values in the attribute. @Asset.Attribute() def Friendly_Name_Example(self, metadata): return self.select_metadata(metadata, name_contains = r'^Compressor Power$', friendly_name ='Power') This attribute method has been modified to make a method call to select_metadata. It passes the metadata, name_contains, which is the search criteria for finding the name value in the metadata "Name" column, and a friendly name. In this example, the attribute will be named "Power". @Asset.Attribute() def No_Friendly_Name_Example(self, metadata): return self.select_metadata(metadata, r'^Compressor Power$', None) If the friendly name does not need to be different from the name_contains parameter without the regex formatting (i.e., special characters), it will name the attribute the search criteria name minus special characters. In the example above the attribute will be named "Compressor Power". This naming method is fairly simple and is not dynamic enough to insert result of the regex search. In those cases, the friendly_name should be specified. The subsequent method call to determine_attribute_type determines the attribute type (e.g., signal, string, scalar). If multiple matches are found, it will select the first signal type over a scalar or string. If attribute is not found, it creates an empty attribute. The final method call to generic_metadata_function determines the friendly name to be used and selects the correct formula for the proper attribute type. As I said earlier, this is more of a trick that address most common issues I have faced when dealing with complex asset trees and applying a common formula in bulk. This can also be applied even if you don't want to apply a formula to all signals. In that case, you would simply modify the formula to be "$signal" instead of "$signal.validValues()". You can still preferentially select signals types over string or scalar types. However, if referencing one of these attributes in a calculated attribute later, you will still have to use the attribute method name (e.g., self.Friendly_Name_Example() when referencing the 'Power' attribute in the formula parameter). build_df = spy.assets.build(AssetTreeName,metadata) # Check for success for each tag for an asset build_df.head(15) After creating our asset structure and attributes, you can run this cell to build our asset tree. I recommend visually checking for success in the 'Build Result' column of the dataframe, looking for 'Success' in each attribute for a single asset. workbookid = "WORKBOOK_ID" # MODIFY: "WORKBOOK_ID" # If you want the tree to be available globally use None as the workbookid push_results_df = spy.push(metadata=build_df, workbook=workbookid, errors='catalog') # Check for success at Push Result push_results_df.head(15) Once you are satisfied with your results or have resolved any errors, you can push your asset tree. If you wish to push your tree globally, change the value of the 'workbook' argument in spy.push to be set to None. I recommend doing this only after addressing all the issues in a workbook, which can serve as your sandbox environment. errors = push_results_df[push_results_df['Push Result'] != "Success"] errors The final step I perform is to check if any of my attributes were not successfully pushed. If there were no issues, the error dataframe should contain no rows. Well, if you have made it to the end, congrats! If you skipped to the end, that's fine too. In this guide, we delved into the world of Asset Templates and their role in streamlining the creation and management of asset trees within Seeq Data Lab. By leveraging object-oriented design principles, encapsulation, and inheritance, Asset Templates offer a powerful way to build complex hierarchical structures. Whether you're dealing with an existing asset tree or starting from scratch, the insights shared here aim to maximize the efficiency of your workflow. Download the attached Jupyter notebook and experiment with the code by modifying or adding new attributes or applying it to a different asset tree. Remember, the best way to grasp these concepts is to dive into the provided Jupyter notebook, make modifications, and witness the results firsthand. In the comments below, I will add some screenshots of common errors I come across when using spy.assets.build and what you should do to troubleshoot the errors. Tips Tricks for Existing Asset Trees.ipynb
    1 point
  4. Troubleshooting common errors when using Asset Templates: Spy.Assets.Build Error 1: No matching metadata row found Error - Key Phrase: "No matching metadata row found" for Temperature Unique Raw Tag on Level_4 class Fix - Modify the search criteria of the attribute shown in the error Error 2: Component dependency not built (calculated tag) Errors - Key phrases: Component Dependency not built - A calculated signal could not be created because the formula parameter referenced was not created. Attribute dependency not built - No matching metadata row found for the referenced formula parameter. Fix - Modify the search criteria of the attributes referenced by the calculated signals Error 3: Component dependency not built (branch not built) Error: Key Phrases: name 'Level 4' is not defined. Unit Component [on AssetTreeName class] component dependency not built. This is because the referenced template in def Unit_Component in the AssetTreeClass does not exist. The class name for the child branch is actually "Not_Level_4". Fix- Change the template to match the actual class name. Example: Code causing error: class AssetTreeName(Asset): @Asset.Component() def Unit_Component(self, metadata): return self.build_components( template=Level_4, metadata=metadata, column_name='Level_3' ) class Not_Level_4(Asset): @Asset.Attribute() def Temperature_Unique_Raw_Tag(self, metadata): return metadata[metadata['Name'] == 'Temperature Unique Raw Tag'].iloc[[0]] Corrected Code: class Level_4(Asset): @Asset.Attribute() def Temperature_Unique_Raw_Tag(self, metadata): return metadata[metadata['Name'] == 'Temperature Unique Raw Tag'].iloc[[0]] Error 4: Multiple attributes returned Error - Key Phrase: "Multiple attributes returned" - This indicates that your search criteria for the attribute returned multiple results. In this case, there a match in ">> Area A" and a match in ">> Area A >> Area A-Duplicate". Fix - Use .iloc[[0]] at the end of the metadata search criteria to select the first match. Original: metadata[metadata['Name'].str.contains(r'(?:Temperature)')] Revised: metadata[metadata['Name'].str.contains(r'(?:Temperature)')].iloc[[0]] Alternatively, you can add more criteria to the search to filter down to the desired tag: metadata[metadata['Name'].str.contains(r'(?:Temperature)')] & metadata[metadata['Level_4'].str.contains(‘Duplicate’) Error 5: ‘class name’ object has no attribute ‘attribute method name’ for calculated attributes Error: Key Phrases: "in Temperature_Cond ‘Formula Parameters’ lies the error. For the formula parameter, we have misspelled the attribute method name in the Level_4 class. Fix – check for misspelled methods or correct method is referenced Error 6: ‘class name’ object has no attribute ‘attribute name’ for roll up calculations Error: Key Phrases: "Cooling_Tower_Max_Temperature" is causing the error. For the roll up calculation, we are trying to reference the self.Temperature_Unique_Raw_Tag() which is in Level_4 which is the child branch. Since this method does not reside in the AssetTreeName class, it cannot find it. Example: Code causing error: class AssetTreeName(Asset): @Asset.Component() def Unit_Component(self, metadata): return self.build_components( template=Level_4, # This the name of the child branch class metadata=metadata, column_name='Level_3' # Metadata column name of the desired asset; Ex: Area A ) # Example Roll Up Calculation @Asset.Attribute() def Cooling_Tower_Max_Temperature(self, metadata): return self.Temperature_Unique_Raw_Tag().roll_up('maximum') Fix - Use the method that builds the child branch (Unit_Component) in the AssetTreeName class and then use the pick function using the friendly name of the tag to be used in the calculation. # Example Roll Up Calculation @Asset.Attribute() def Cooling_Tower_Max_Temperature(self, metadata): return self.Unit_Component().pick({ 'Name': 'Temperature Possible Duplicates Raw Tag' }).roll_up('maximum') Error 7: Spy.Push Errors In this example, I have tried to apply an agileFilter function onto a string attribute. This error can occur when an asset may have a string or scalar attribute exist instead of a signal attribute when a field instrument does not exist for this asset while a sibling asset would have a signal attribute and you want to apply the agileFilter. It can be difficult to determine which attributes are causing the issue. You can catalog the errors and allow spy.push to continue to push the Asset Tree without the errors and cataloging the errors. push_results_df = spy.push(metadata=build_df, workbook=workbookid, errors='catalog') The error will be cataloged in the "Push Result" column with same error shown in the example. To view all errors, you can filter the push result to see where the push result was not success. error = push_results_df[push_results_df['Push Result'] != "Success"] error
    1 point
  5. When I use the spy.push() command to insert some capsules into a worksheet it overwrites the existing worksheet rather than just inserting into it. The capsules are made correctly but all other data in the worksheet disappears. I've read through the docs and can't see anywhere where I need to specify to insert and not overwrite. Hoping someone could point me in the right direction. spy.push(data=CapsuleData, metadata=pd.DataFrame([{ 'Name': 'Capsules Times', 'Type': 'Condition', 'Maximum Duration': '1d' }]), workbook='Practice', worksheet='Practice Worksheet')
    1 point
This leaderboard is set to Los Angeles/GMT-08:00
×
×
  • Create New...