class MetadataCatalog
Helper class to manage FileCloud metadata sets and attributes. This class provides methods to work with FileCloud metadata by providing a more user-friendly interface on top of the raw API.
/tf/active/vicechatdev/metadata_catalog.py
9 - 1398
moderate
Purpose
Helper class to manage FileCloud metadata sets and attributes. This class provides methods to work with FileCloud metadata by providing a more user-friendly interface on top of the raw API.
Source Code
class MetadataCatalog:
"""
Helper class to manage FileCloud metadata sets and attributes.
This class provides methods to work with FileCloud metadata by
providing a more user-friendly interface on top of the raw API.
"""
def __init__(self, api_client: FileCloudAPI, debug: bool = False):
"""
Initialize the metadata catalog.
Args:
api_client: Authenticated FileCloudAPI client
debug: Enable detailed debug output
"""
self.api = api_client
self.sets = {}
self.initialized = False
self.debug = debug
def _debug_print(self, message: str, data: Any = None):
"""
Print debug information if debug mode is enabled.
Args:
message: Debug message
data: Optional data to print
"""
if self.debug:
print(f"[MetadataCatalog Debug] {message}")
if data is not None:
if isinstance(data, dict) or isinstance(data, list):
try:
print(json.dumps(data, indent=2))
except:
print(data)
else:
print(data)
def _ensure_initialized(self):
"""
Ensure the catalog is initialized.
"""
if not self.initialized:
self.initialize()
def initialize(self):
"""
Initialize the metadata catalog by loading all metadata sets and attributes.
This version uses a single API call to get all metadata sets with their attributes.
"""
self.metadata_sets = {}
self.metadata_attributes = {}
# Get all metadata sets in one API call
result = self.api.get_metadata_sets()
if not result.get('success'):
logger.error(f"Failed to load metadata sets: {result.get('message', 'Unknown error')}")
return False
try:
# Extract metadata sets from the response
metadata_sets_data = result.get('data', {}).get('metadatasets', {}).get('metadataset', [])
if not metadata_sets_data:
logger.warning("No metadata sets found")
return False
# Process each metadata set
for metadata_set in metadata_sets_data:
set_id = metadata_set.get('id')
set_name = metadata_set.get('name')
if not set_id:
continue
# Store metadata set information
self.metadata_sets[set_id] = {
'id': set_id,
'name': set_name,
'description': metadata_set.get('description', ''),
'type': metadata_set.get('type', ''),
'attributes': {} # Will store attributes for this set
}
# Process attributes for this set
attributes_total = int(metadata_set.get('attributes_total', 0))
for i in range(attributes_total):
# Extract attribute properties using the pattern in the response
attribute_id = metadata_set.get(f'attribute{i}_attributeid')
if not attribute_id:
continue
attribute_name = metadata_set.get(f'attribute{i}_name', '')
attribute_type = metadata_set.get(f'attribute{i}_type', '1')
attribute_description = metadata_set.get(f'attribute{i}_description', '')
attribute_default = metadata_set.get(f'attribute{i}_defaultvalue', '')
attribute_required = metadata_set.get(f'attribute{i}_required', '0') == '1'
attribute_disabled = metadata_set.get(f'attribute{i}_disabled', '0') == '1'
# Process predefined values if any
predefined_values = []
predefined_values_total = int(metadata_set.get(f'attribute{i}_predefinedvalues_total', 0))
for j in range(predefined_values_total):
predefined_value = metadata_set.get(f'attribute{i}_predefinedvalue{j}', '')
if predefined_value:
predefined_values.append(predefined_value)
# Create attribute object
attribute = {
'id': attribute_id,
'name': attribute_name,
'description': attribute_description,
'type': self._get_attribute_type_name(attribute_type),
'type_id': attribute_type,
'default': attribute_default,
'required': attribute_required,
'disabled': attribute_disabled,
'predefined_values': predefined_values,
'set_id': set_id,
'set_name': set_name
}
# Store in both collections
self.metadata_attributes[attribute_id] = attribute
self.metadata_sets[set_id]['attributes'][attribute_id] = attribute
logger.info(f"Successfully loaded {len(self.metadata_sets)} metadata sets with {len(self.metadata_attributes)} attributes")
self.initialized=True
return True
except Exception as e:
logger.error(f"Error initializing metadata catalog: {e}")
import traceback
logger.error(traceback.format_exc())
return False
def _get_attribute_type_name(self, type_id):
"""Map attribute type ID to a readable name."""
type_map = {
'1': 'Text',
'2': 'Integer',
'3': 'Decimal',
'4': 'Boolean',
'5': 'Date',
'6': 'Select',
'7': 'Tags',
'8': 'User',
'9': 'Color'
}
return type_map.get(str(type_id), 'Unknown')
def list_metadata_sets(self) -> List[Dict]:
"""
List all available metadata sets.
Returns:
List[Dict]: List of metadata sets with their information
"""
self._ensure_initialized()
return [{"id": set_id, **set_info} for set_id, set_info in self.metadata_sets.items()]
def list_attributes(self, set_id: str) -> List[Dict]:
"""
List all attributes in a specific metadata set.
Args:
set_id: ID of the metadata set
Returns:
List[Dict]: List of attributes with their information
"""
self._ensure_initialized()
if set_id not in self.metadata_sets:
return []
attributes = self.metadata_sets[set_id].get('attributes', {})
return [{"id": attr_id, **attr_info} for attr_id, attr_info in attributes.items()]
def get_metadata_set_by_name(self, set_name: str) -> Optional[Dict]:
"""
Get a metadata set by its name.
Args:
set_name: Name of the metadata set to find
Returns:
Optional[Dict]: Metadata set information or None if not found
"""
self._ensure_initialized()
for set_id, set_info in self.metadata_sets.items():
if set_info.get('name') == set_name:
return {
"id": set_id,
**set_info
}
return None
def get_metadata_set_by_id(self, set_id: str) -> Optional[Dict]:
"""
Get a metadata set by its ID.
Args:
set_id: ID of the metadata set to find
Returns:
Optional[Dict]: Metadata set information or None if not found
"""
self._ensure_initialized()
if set_id in self.metadata_sets:
return {
"id": set_id,
**self.metadata_sets[set_id]
}
return None
def get_attribute_by_name(self, set_id: str, attribute_name: str) -> Optional[Dict]:
"""
Get a metadata attribute by its name within a specific set.
Args:
set_id: ID of the metadata set
attribute_name: Name of the attribute to find
Returns:
Optional[Dict]: Attribute information or None if not found
"""
self._ensure_initialized()
if set_id not in self.metadata_sets:
return None
attributes = self.metadata_sets[set_id].get('attributes', {})
for attr_id, attr_info in attributes.items():
if attr_info.get('name') == attribute_name:
return {
"id": attr_id,
**attr_info
}
return None
def get_attribute_by_id(self, set_id: str, attribute_id: str) -> Optional[Dict]:
"""
Get a metadata attribute by its ID within a specific set.
Args:
set_id: ID of the metadata set
attribute_id: ID of the attribute to find
Returns:
Optional[Dict]: Attribute information or None if not found
"""
self._ensure_initialized()
if set_id not in self.metadata_sets:
return None
attributes = self.metadata_sets[set_id].get('attributes', {})
if attribute_id in attributes:
return {
"id": attribute_id,
**attributes[attribute_id]
}
return None
def get_metadata_values(self, file_path: str) -> Dict:
"""
Get metadata values for a specific file in a structured format.
Supports multiple metadata sets per file.
Args:
file_path: Path to the file
Returns:
Dict: Structured metadata values with the following format:
{
'success': bool,
'file_path': str,
'sets': [
{
'id': str,
'name': str,
'description': str,
'attributes': [
{
'id': str,
'name': str,
'description': str,
'type': str,
'value': str,
'required': bool,
'disabled': bool
},
...
]
},
...
],
'raw_response': dict # Original API response
}
"""
# Get raw metadata values from API
raw_response = self.api.get_metadata_values(file_path)
# Initialize result structure
result = {
'success': raw_response.get('success', False),
'file_path': file_path,
'sets': [],
'raw_response': raw_response
}
# Return early if API call was not successful
if not result['success']:
return result
try:
# Extract metadata sets info and attributes
metadata_values = raw_response.get('data', {}).get('metadatavalues', {})
# Check if we have multiple sets or a single set
metadata_sets = metadata_values.get('metadatasetvalue', [])
if not isinstance(metadata_sets, list):
metadata_sets = [metadata_sets] # Convert to list for consistent processing
# Process each metadata set
for metadata_set in metadata_sets:
if not metadata_set:
continue
# Extract metadata set information
set_info = {
'id': metadata_set.get('id', ''),
'name': metadata_set.get('name', ''),
'description': metadata_set.get('description', ''),
'attributes': []
}
# Extract attributes
attributes_total = int(metadata_set.get('attributes_total', 0))
for i in range(attributes_total):
attribute = {
'id': metadata_set.get(f'attribute{i}_attributeid', ''),
'name': metadata_set.get(f'attribute{i}_name', ''),
'description': metadata_set.get(f'attribute{i}_description', ''),
'type': self._get_attribute_type_name(metadata_set.get(f'attribute{i}_datatype', '1')),
'type_id': metadata_set.get(f'attribute{i}_datatype', '1'),
'value': metadata_set.get(f'attribute{i}_value', ''),
'required': metadata_set.get(f'attribute{i}_required', '0') == '1',
'disabled': metadata_set.get(f'attribute{i}_disabled', '0') == '1'
}
set_info['attributes'].append(attribute)
result['sets'].append(set_info)
# For backward compatibility, add the first set as 'set' and its attributes as 'attributes'
if result['sets']:
result['set'] = result['sets'][0].copy()
del result['set']['attributes'] # Remove attributes from the set copy
result['attributes'] = result['sets'][0]['attributes']
except Exception as e:
logger.error(f"Error processing metadata response: {str(e)}")
logger.error(traceback.format_exc())
result['error'] = str(e)
return result
def get_attribute_by_name_from_meta(self, metadata: Dict, name: str) -> Optional[Dict]:
"""
Get an attribute from metadata results by its name.
Args:
metadata: Metadata dictionary returned by get_metadata_values()
name: Name of the attribute to find
Returns:
Dict or None: The attribute dictionary or None if not found
"""
if not metadata or not isinstance(metadata, dict):
return None
attributes = metadata.get('attributes', [])
for attr in attributes:
if attr.get('name') == name:
return attr
return None
def get_attributes_as_dict(self, metadata: Dict) -> Dict[str, Dict]:
"""
Convert the attributes list to a dictionary keyed by attribute names.
Args:
metadata: Metadata dictionary returned by get_metadata_values()
Returns:
Dict: Dictionary of attributes with attribute names as keys
"""
if not metadata or not isinstance(metadata, dict):
return {}
attributes = metadata.get('attributes', [])
return {attr.get('name'): attr for attr in attributes if 'name' in attr}
def get_attribute_values(self, metadata: Dict) -> Dict[str, str]:
"""
Extract just the attribute names and values as a simple dictionary.
Combines attributes from all metadata sets.
Args:
metadata: Metadata dictionary returned by get_metadata_values()
Returns:
Dict: Dictionary with attribute names as keys and their values as values
"""
if not metadata or not isinstance(metadata, dict):
return {}
attributes = {}
# Check if we're using the new multi-set format
if 'sets' in metadata and isinstance(metadata['sets'], list):
for set_info in metadata['sets']:
for attr in set_info.get('attributes', []):
if 'name' in attr:
attributes[attr.get('name')] = attr.get('value', '')
# Fallback to the old single-set format for backward compatibility
elif 'attributes' in metadata and isinstance(metadata['attributes'], list):
for attr in metadata['attributes']:
if 'name' in attr:
attributes[attr.get('name')] = attr.get('value', '')
return attributes
def save_attribute_values(self, file_path: str, metadata: Dict) -> Dict:
"""
Save metadata values for a specific file using structured format.
Args:
file_path: Path to the file
metadata: Structured metadata values in the format returned by get_metadata_values.
Must contain at least 'set' with 'id' and 'attributes' list with 'id' and 'value'.
{
'set': {'id': 'set_id'},
'attributes': [
{'id': 'attr1_id', 'value': 'new_value1'},
{'id': 'attr2_id', 'value': 'new_value2'},
...
]
}
Returns:
Dict: Response from the API with additional structured information
"""
# Validate input
if not isinstance(metadata, dict):
return {'success': False, 'message': 'Invalid metadata format'}
set_info = metadata.get('set', {})
set_id = set_info.get('id')
if not set_id:
return {'success': False, 'message': 'Missing metadata set ID'}
attributes = metadata.get('attributes', [])
if not attributes:
return {'success': False, 'message': 'No attributes provided'}
# Convert the structured attributes to the format expected by FileCloud API
attribute_values = {}
for attr in attributes:
attr_id = attr.get('id')
attr_value = attr.get('value')
if attr_id and attr_value is not None: # Allow empty string values
attribute_values[attr_id] = str(attr_value)
# If no valid attribute values, return error
if not attribute_values:
return {'success': False, 'message': 'No valid attribute values provided'}
# Call the API to save the attribute values
api_response = self.api.save_attribute_values(file_path, set_id, attribute_values)
# Return structured response
return {
'success': api_response.get('success', False),
'file_path': file_path,
'set_id': set_id,
'updated_attributes': len(attribute_values),
'message': api_response.get('message', ''),
'raw_response': api_response
}
def save_attribute_values_by_name(self, file_path: str, set_name: str, attributes: Dict[str, str]) -> Dict:
"""
Save metadata values using set name and attribute names instead of IDs.
This is a convenience method that translates human-readable names to IDs and then
calls save_attribute_values with the properly formatted input.
Args:
file_path: Path to the file
set_name: Name of the metadata set
attributes: Dictionary with attribute names as keys and their values as values
{
'attr_name1': 'value1',
'attr_name2': 'value2',
...
}
Returns:
Dict: Response from the API with additional structured information
"""
self._ensure_initialized()
# Initialize result structure
result = {
'success': False,
'file_path': file_path,
'set_name': set_name,
'attributes': attributes,
'message': '',
'raw_response': None
}
# Find the set ID from the name
set_info = self.get_metadata_set_by_name(set_name)
if not set_info:
result['message'] = f"Metadata set with name '{set_name}' not found"
self._debug_print(f"Error: {result['message']}")
return result
set_id = set_info['id']
# Convert attribute names to IDs
attribute_values = {}
for attr_name, value in attributes.items():
attr_info = self.get_attribute_by_name(set_id, attr_name)
if attr_info:
attribute_values[attr_info['id']] = str(value) if value is not None else ""
self._debug_print(f"Mapped attribute {attr_name} to ID {attr_info['id']} with value {value}")
else:
self._debug_print(f"Warning: Attribute {attr_name} not found in set {set_name}")
# We could skip this attribute, but let's keep it with the name as ID
# for better error handling in the API
attribute_values[attr_name] = str(value) if value is not None else ""
if not attribute_values:
result['message'] = f"No valid attributes found for metadata set '{set_name}'"
self._debug_print(f"Error: {result['message']}")
return result
# Call API to save the attribute values
self._debug_print(f"Saving attributes for file {file_path}, set ID {set_id}")
self._debug_print("Attribute values:", attribute_values)
raw_response = self.api.save_attribute_values(file_path, set_id, attribute_values)
result['raw_response'] = raw_response
# Update result based on API response
result['success'] = raw_response.get('success', False)
if not result['success']:
result['message'] = raw_response.get('message', 'Unknown error')
self._debug_print(f"API error: {result['message']}")
else:
result['message'] = 'Attributes saved successfully'
self._debug_print("Attributes saved successfully")
return result
def create_search_attributes(self, search_criteria: List[Dict]) -> List[Dict]:
"""
Create a list of search attributes formatted for the API, translating
friendly names to IDs.
Args:
search_criteria: A list of dictionaries with keys 'set_name',
'attribute_name', and 'value'
Returns:
List[Dict]: Formatted attributes for search_metadata API call
"""
self._ensure_initialized()
formatted_attributes = []
for criteria in search_criteria:
set_name = criteria.get('set_name')
attribute_name = criteria.get('attribute_name')
value = criteria.get('value')
# Skip if any required field is missing
if not set_name or not attribute_name or value is None:
continue
# Find the set by name
metadata_set = self.get_metadata_set_by_name(set_name)
if not metadata_set:
print(f"Warning: Metadata set '{set_name}' not found")
continue
set_id = metadata_set.get('id')
# Find the attribute by name
attribute = self.get_attribute_by_name(set_id, attribute_name)
if not attribute:
print(f"Warning: Attribute '{attribute_name}' not found in set '{set_name}'")
continue
attribute_id = attribute.get('id')
type_id = attribute.get('type_id')
# Create the formatted attribute
formatted_attr = {
'setid': set_id,
'type_id': type_id,
'attributeid': attribute_id,
'value': value
}
# Add operator if specified (default to 'equals')
if 'operator' in criteria:
formatted_attr['operator'] = criteria['operator']
else:
formatted_attr['operator'] = 'equals'
formatted_attributes.append(formatted_attr)
return formatted_attributes
def search_files_by_metadata(self, search_criteria: List[Dict],
search_string: str = "**",
search_location: Optional[str] = None) -> List[str]:
"""
Search for files that match specific metadata criteria.
Args:
search_criteria: A list of dictionaries with keys 'set_name',
'attribute_name', 'value', and optionally 'operator'
search_string: Search string for filename pattern (use "**" for all files)
search_location: Optional path to limit search to
Returns:
List[str]: List of file paths that match the criteria
"""
# Create formatted search attributes
search_attributes = self.create_search_attributes(search_criteria)
if not search_attributes:
print("Warning: No valid search attributes could be created")
return []
# Perform the search
result = self.api.search_metadata(
search_string=search_string,
search_scope='3', # All files/folders
attributes=search_attributes,
#search_location=search_location
)
# Return paths or empty list if not successful
return result.get('paths', [])
def create_metadata_editor(self, file_path: str = None, metadata: Dict = None,
views: List[str] = None):
"""
Create an interactive panel for editing file metadata.
This method creates a Holoviz Panel interface for viewing and editing
metadata values. Once created, the panel can be displayed in a notebook
or web application.
Args:
file_path: Optional path to the file whose metadata to edit.
If provided and metadata is None, metadata will be loaded
from this file.
metadata: Optional pre-loaded metadata to edit. If provided,
file_path is only used when saving changes.
views: Optional list of views to display, choose from ['form', 'table', 'json']
Default is ['form'] if not specified
Returns:
panel.Column: Panel layout that can be displayed in notebooks or web apps
"""
try:
import panel as pn
import pandas as pd
from panel.widgets import Button, Select, TextInput, DataFrame, TextAreaInput, Switch
from panel.layout import Column, Row, Tabs, HSpacer
# Load panel extensions
pn.extension()
except ImportError:
raise ImportError(
"This feature requires Panel and pandas. Install with: "
"pip install panel==1.6.1 pandas"
)
# Default to just the form view if not specified
if views is None:
views = ['form']
# Define common styles for consistent look and feel
STYLES = {
"container": {"background": "#f8f9fa", "padding": "10px", "border-radius": "5px"},
"header": {"background": "#4a86e8", "color": "white", "padding": "10px",
"font-weight": "bold", "border-radius": "5px 5px 0 0"},
"label": {"font-weight": "bold", "margin-bottom": "5px", "color": "#333333"},
"input": {"background": "#ffffff", "border": "1px solid #ddd",
"padding": "8px", "border-radius": "4px", "width": "100%",
"margin-bottom": "10px", "color": "#333333"},
"button_primary": {"background": "#4a86e8", "color": "white"},
"button_success": {"background": "#28a745", "color": "white"},
"message": {"padding": "10px", "border-radius": "4px",
"font-weight": "bold", "margin-top": "10px"},
"description": {"font-style": "italic", "color": "#666", "margin-left": "10px"}
}
# Create container for dynamic content with improved styling
message_area = pn.pane.Markdown("", styles=STYLES["message"])
# Function to load metadata
def load_metadata(path):
nonlocal metadata
if not path:
message_area.object = "â ī¸ Please enter a valid file path"
message_area.styles = {**STYLES["message"], "background": "#fff3cd", "color": "#856404"}
return None
try:
md = self.get_metadata_values(path)
if not md.get('success', False):
error_msg = md.get('message', 'Unknown error')
message_area.object = f"â Error loading metadata: {error_msg}"
message_area.styles = {**STYLES["message"], "background": "#f8d7da", "color": "#721c24"}
return None
message_area.object = f"â
Metadata loaded for {path}"
message_area.styles = {**STYLES["message"], "background": "#d4edda", "color": "#155724"}
return md
except Exception as e:
message_area.object = f"â Error: {str(e)}"
message_area.styles = {**STYLES["message"], "background": "#f8d7da", "color": "#721c24"}
return None
# Create file path input with better styling
file_path_input = TextInput(
name="File Path",
value=file_path or "",
placeholder="/path/to/file",
width=600,
styles={"background": "#ffffff", "color": "#333333", "border": "1px solid #ddd"}
)
# Load button with better styling
load_button = Button(
name="Load Metadata",
button_type="primary",
width=150,
styles=STYLES["button_primary"]
)
# Tabs for different views
tabs = pn.Tabs()
# Define editors dictionary at a higher scope so it's available to all functions
editors = {}
# Function to update display when metadata changes
def update_display(md):
if not md:
return
# Clear existing tabs
tabs.clear()
# Clear existing editors
nonlocal editors
editors = {}
# Create DataFrame for table view
attrs_list = []
if 'attributes' in md:
for attr in md['attributes']:
attrs_list.append({
'Name': attr.get('name', ''),
'Value': attr.get('value', ''),
'Description': attr.get('description', ''),
'Type': attr.get('type', ''),
'Required': 'â' if attr.get('required', False) else '',
'ID': attr.get('id', '')
})
df = pd.DataFrame(attrs_list)
# Create editors based on attribute types
form_layout = []
# Add set information with better styling
set_info = md.get('set', {})
set_name = set_info.get('name', 'Unknown Set')
set_desc = set_info.get('description', '')
# Add set header with improved styling
header = pn.pane.Markdown(f"# {set_name}", styles=STYLES["header"])
form_layout.append(header)
if set_desc:
form_layout.append(pn.pane.Markdown(f"*{set_desc}*", styles={"padding": "5px 10px", "color": "#333333"}))
# Add container for form fields with consistent styling
form_container = pn.Column(styles=STYLES["container"])
if 'attributes' in md:
# Sort attributes by name
sorted_attrs = sorted(md['attributes'], key=lambda x: x.get('name', ''))
# Create form fields for each attribute
for attr in sorted_attrs:
attr_name = attr.get('name', 'Unnamed')
attr_id = attr.get('id', '')
attr_value = attr.get('value', '')
attr_type = attr.get('type', '')
attr_desc = attr.get('description', '')
attr_required = attr.get('required', False)
attr_disabled = attr.get('disabled', False)
# Create field label with required indicator if needed
field_label = f"{attr_name}" + (" *" if attr_required else "")
label_widget = pn.pane.Markdown(
f"**{field_label}**",
styles={"font-weight": "bold", "color": "#333333", "margin-bottom": "5px"},
width=200
)
# Create appropriate editor based on type with consistent styling
if attr_type == 'Boolean':
# Boolean switch with better styling
editor = Switch(
name='', # Remove duplicate name since we have a separate label
value=attr_value.lower() in ('true', 'yes', '1'),
disabled=attr_disabled,
styles={"margin-top": "8px"}
)
elif attr_type == 'Text' and len(attr_value) > 50:
# Text area for longer text with better styling
editor = TextAreaInput(
name='', # Remove duplicate name
value=attr_value,
placeholder=f"Enter {attr_name}...",
disabled=attr_disabled,
height=100,
width=400,
styles={"background": "#ffffff", "color": "#333333", "border": "1px solid #ddd"}
)
else:
# Default to text input with better styling
editor = TextInput(
name='', # Remove duplicate name
value=attr_value,
placeholder=f"Enter {attr_name}...",
disabled=attr_disabled,
width=400,
styles={"background": "#ffffff", "color": "#333333", "border": "1px solid #ddd"}
)
# Store editor with attribute ID for later access
editors[attr_id] = editor
# Create a row with label, editor, and description if available
row_items = [label_widget, editor]
if attr_desc:
desc_widget = pn.pane.Markdown(
f"*{attr_desc}*",
styles={"font-style": "italic", "color": "#666666", "margin-left": "10px"},
width=200
)
row_items.append(desc_widget)
# Create a consistent row with better alignment
field_row = pn.Row(
*row_items,
align='center',
styles={"margin-bottom": "5px", "padding": "5px 0"}
)
form_container.append(field_row)
# Add form container to layout
form_layout.append(form_container)
# Save function
def save_changes(event):
try:
save_path = file_path_input.value
if not save_path:
message_area.object = "â ī¸ Please enter a file path to save metadata"
message_area.styles = {**STYLES["message"], "background": "#fff3cd", "color": "#856404"}
return
# Check if we need to sync data from table view first
active_tab = tabs.active
if active_tab == 1 and 'table' in views: # If we're in table view
try:
# Get DataFrame from table view - Fix the access path to the DataFrame
# Access the DataFrame directly as it's the 4th item (index 3) in the table container
table_container = tabs[1]
df_widget = None
# Loop through the objects to find the DataFrame widget
for obj in table_container:
if isinstance(obj, pn.widgets.DataFrame):
df_widget = obj
break
if df_widget is not None:
table_df = df_widget.value
# Update form editors with values from table
for _, row in table_df.iterrows():
attr_id = row['ID']
new_value = row['Value']
# Only update if the editor for this attribute exists
if attr_id in editors:
# Find attribute in metadata to get its type
attr_type = next((attr['type'] for attr in md['attributes']
if attr['id'] == attr_id), None)
if attr_type == 'Boolean':
editors[attr_id].value = new_value.lower() in ('true', 'yes', '1')
else:
editors[attr_id].value = new_value
except Exception as e:
print(f"Error syncing table data during save: {e}")
# Update metadata values from editors
updated_md = md.copy()
for attr in updated_md.get('attributes', []):
attr_id = attr.get('id')
if attr_id in editors:
editor = editors[attr_id]
new_value = str(editor.value)
# Convert boolean values to string format expected by API
if attr.get('type') == 'Boolean' and isinstance(editor.value, bool):
new_value = 'true' if editor.value else 'false'
attr['value'] = new_value
# Save changes
result = self.save_attribute_values(save_path, updated_md)
if result.get('success'):
message_area.object = f"â
Saved metadata to {save_path}"
message_area.styles = {**STYLES["message"], "background": "#d4edda", "color": "#155724"}
# If saving from table view, also update the table view
if active_tab == 1 and 'table' in views:
# Refresh the form data in metadata
metadata.update(updated_md)
# Update table data - use the same logic as above to find the DataFrame
table_container = tabs[1]
df_widget = None
for obj in table_container:
if isinstance(obj, pn.widgets.DataFrame):
df_widget = obj
break
if df_widget is not None:
table_df = df_widget.value
for i, row in table_df.iterrows():
attr_id = row['ID']
for attr in updated_md.get('attributes', []):
if attr.get('id') == attr_id:
table_df.at[i, 'Value'] = attr.get('value', '')
break
# Set updated dataframe back to widget
df_widget.value = table_df
else:
message_area.object = f"â Error saving metadata: {result.get('message', 'Unknown error')}"
message_area.styles = {**STYLES["message"], "background": "#f8d7da", "color": "#721c24"}
except Exception as e:
message_area.object = f"â Error: {str(e)}"
message_area.styles = {**STYLES["message"], "background": "#f8d7da", "color": "#721c24"}
# Create save button with better styling
save_button = Button(
name="Save Changes",
button_type="success",
width=150,
styles=STYLES["button_success"]
)
save_button.on_click(save_changes)
# Add save button at the bottom of the form with proper spacing
form_layout.append(pn.layout.Spacer(height=20))
form_layout.append(pn.Row(save_button, align='center'))
# Create tabs based on requested views
if 'form' in views:
form_panel = Column(*form_layout, scroll=True, width=850)
tabs.append(("Form View", form_panel))
# Only add table view if requested and there are attributes
if 'table' in views and len(attrs_list) > 0:
# 1. Create value-only editable columns dictionary
value_columns = {'Value': {'editable': True}}
other_columns = {col: {'editable': False} for col in df.columns if col != 'Value'}
editable_columns = {**other_columns, **value_columns}
# 2. Create styled DataFrame widget for table view
df_widget = DataFrame(
df,
width=830,
height=400,
show_index=False,
editors=editable_columns, # Apply column-specific editability
# Use CSS for styling the table properly
styles={
'color': '#333333',
'background-color': 'white'
}
)
# 3. Improved table styling with custom CSS
table_css = """
<style>
.metadata-table .bk-data-table {
color: #333333;
background-color: white;
}
.metadata-table .bk-data-table th {
background-color: #f2f2f2;
color: #333333;
font-weight: bold;
border-bottom: 1px solid #ddd;
padding: 8px;
}
.metadata-table .bk-data-table td {
color: #333333;
background-color: white;
border-bottom: 1px solid #f0f0f0;
padding: 6px 8px;
}
.metadata-table .slick-row:nth-child(odd) {
background-color: #f9f9f9;
}
.metadata-table .slick-cell.active {
border: 2px solid #4a86e8;
}
</style>
"""
table_style_pane = pn.pane.HTML(table_css, width=0, height=0, margin=0)
# Create table container with styling
table_container = Column(
pn.pane.Markdown("## Metadata Table View", styles=STYLES["header"]),
table_style_pane, # Include the CSS styling
pn.pane.Markdown(
"âī¸ *Only the Value column can be edited. Click Save Changes below to save your changes.*",
styles={"color": "#666666", "font-style": "italic", "margin": "5px 0 10px 0"}
),
df_widget,
pn.layout.Spacer(height=20), # Add space before the button
pn.Row(
Button(
name="Save Changes",
button_type="success",
width=150,
styles=STYLES["button_success"],
on_click=save_changes # Use the same save function
),
align='center'
),
css_classes=['metadata-table'], # Apply custom CSS class
styles=STYLES["container"]
)
tabs.append(("Table View", table_container))
# Add an improved JSON view for developers if requested
if 'json' in views:
# Create a formatted, collapsible JSON tree view
formatted_json = {
"set": md.get('set', {}),
"attributes": {attr.get('name', f'attr_{i}'): {
"id": attr.get('id', ''),
"value": attr.get('value', ''),
"type": attr.get('type', ''),
"description": attr.get('description', ''),
"required": attr.get('required', False),
"disabled": attr.get('disabled', False)
} for i, attr in enumerate(md.get('attributes', []))}
}
json_tree_widget = pn.widgets.JSONEditor(
value=formatted_json,
width=830,
height=500,
styles={"color": "#333333", "background": "white", "font-family": "monospace"}
)
# Add instructions for using the JSON view
json_help = pn.pane.Markdown(
"""
âšī¸ **JSON View Help**
- Click the âļ arrows to expand/collapse sections
- This view provides a better organized representation of the metadata
- The attributes are organized by name for easier reference
- Changes in this view won't be saved to FileCloud
""",
styles={"background": "#e9f5ff", "color": "#333", "padding": "10px", "border-radius": "4px", "margin-bottom": "10px"}
)
json_container = Column(
pn.pane.Markdown("## JSON View", styles=STYLES["header"]),
json_help,
json_tree_widget,
styles=STYLES["container"]
)
tabs.append(("JSON", json_container))
# Wire up the load button
def load_click(event):
md = load_metadata(file_path_input.value)
if md:
nonlocal metadata
metadata = md
update_display(md)
load_button.on_click(load_click)
# Function to sync table data back to form editors when switching tabs
def sync_table_to_form(event):
# Only sync if we're coming from the table view tab to the form view
if event.new == 0 and event.old == 1 and 'table' in views:
try:
# Get DataFrame from table widget
table_df = tabs[1][0].objects[3].value
# Update form editors with values from table
for _, row in table_df.iterrows():
attr_name = row['Name']
attr_id = row['ID']
new_value = row['Value']
# Only update if the editor for this attribute exists
if attr_id in editors:
# Handle boolean values specially
attr_type = next((attr['type'] for attr in metadata['attributes']
if attr['id'] == attr_id), None)
if attr_type == 'Boolean':
editors[attr_id].value = new_value.lower() in ('true', 'yes', '1')
else:
editors[attr_id].value = new_value
except Exception as e:
print(f"Error syncing table data: {e}")
# Add tab change callback to sync data
tabs.param.watch(sync_table_to_form, 'active')
# Initialize display if metadata was provided
if metadata is None and file_path:
metadata = load_metadata(file_path)
if metadata:
update_display(metadata)
# Create file selector with better alignment
file_selector = Row(
file_path_input,
load_button,
align='center',
styles={"margin": "10px 0"}
)
# Create header with styled title
header = pn.pane.Markdown(
"# FileCloud Metadata Editor",
styles={"color": "#2c3e50", "background": "#ecf0f1", "padding": "10px",
"text-align": "center", "border-radius": "5px"}
)
# Apply better styling to tabs for better readability
tabs_css = """
<style>
/* Improve tab visibility with stronger colors and borders */
.bk-header.bk-tab {
background-color: #e6e6e6;
color: #333333;
font-weight: bold;
border: 1px solid #ccc;
border-bottom: none;
border-radius: 5px 5px 0 0;
padding: 8px 15px;
margin-right: 2px;
}
.bk-header.bk-tab.bk-active {
background-color: #4a86e8;
color: white;
border-color: #3a76d8;
}
.bk-tab-content {
border-top: 2px solid #4a86e8;
padding-top: 10px;
}
</style>
"""
# Build final layout with consistent styling and improved tabs
layout = Column(
header,
file_selector,
message_area,
pn.layout.Divider(),
pn.pane.HTML(tabs_css, width=0, height=0, margin=0), # Add tab styling
tabs,
width=850,
styles={"background": "#f8f9fa", "padding": "15px", "border-radius": "8px"}
)
return layout
def display_metadata_editor(self, file_path: str = None, metadata: Dict = None,
views: List[str] = None):
"""
Create and display an interactive metadata editor in a Jupyter notebook.
Args:
file_path: Optional path to the file whose metadata to edit
metadata: Optional pre-loaded metadata to edit
views: Optional list of views to display, choose from ['form', 'table', 'json']
Default is ['form'] if not specified
Returns:
panel.Column: The displayed panel object
"""
try:
import panel as pn
pn.extension()
except ImportError:
raise ImportError("This feature requires Panel. Install with: pip install panel==1.6.1")
# Default to just the form view if not specified
if views is None:
views = ['form']
editor = self.create_metadata_editor(file_path, metadata, views)
return editor
def debug_attribute_lookup(self, set_id: str, attribute_name: str) -> Dict:
"""
Debug function to help diagnose attribute lookup issues.
Args:
set_id: ID of the metadata set
attribute_name: Name of the attribute to find
Returns:
Dict: Debug information about the lookup
"""
self._ensure_initialized()
result = {
'set_id': set_id,
'attribute_name': attribute_name,
'found': False,
'set_exists': set_id in self.metadata_sets,
'attributes_count': 0,
'available_attributes': [],
'attribute_names_in_set': []
}
if not result['set_exists']:
return result
attributes = self.metadata_sets[set_id].get('attributes', {})
result['attributes_count'] = len(attributes)
# Get all attribute names for debugging
for attr_id, attr_info in attributes.items():
attr_name = attr_info.get('name', '')
result['attribute_names_in_set'].append(attr_name)
result['available_attributes'].append({
'id': attr_id,
'name': attr_name,
'type': attr_info.get('type', '')
})
# Check if we can find the attribute by name
for attr_id, attr_info in attributes.items():
if attr_info.get('name') == attribute_name:
result['found'] = True
result['attribute_id'] = attr_id
result['attribute_info'] = attr_info
break
return result
def add_set_to_file_object(self, file_path: str, set_id: str = None, set_name: str = None) -> Dict:
"""
Add a metadata set to a file or folder.
Args:
file_path: Path to the file or folder
set_id: ID of the metadata set to add (either set_id or set_name must be provided)
set_name: Name of the metadata set to add (used if set_id is not provided)
Returns:
Dict: Response with the following format:
{
'success': bool,
'file_path': str,
'set_id': str,
'set_name': str,
'message': str,
'raw_response': dict # Original API response
}
"""
self._ensure_initialized()
# Initialize result structure
result = {
'success': False,
'file_path': file_path,
'set_id': set_id,
'set_name': set_name,
'message': '',
'raw_response': None
}
# If set_id is not provided, try to find it by name
if not set_id and set_name:
set_info = self.get_metadata_set_by_name(set_name)
if set_info:
set_id = set_info.get('id')
result['set_id'] = set_id
else:
result['message'] = f"Metadata set with name '{set_name}' not found"
return result
# If we still don't have a set_id, we can't proceed
if not set_id:
result['message'] = "Either set_id or set_name must be provided"
return result
# Call the API to add the set to the file object
self._debug_print(f"Adding metadata set '{set_id}' to file '{file_path}'")
raw_response = self.api.add_set_to_file_object(file_path, set_id)
result['raw_response'] = raw_response
# Update result based on API response
result['success'] = raw_response.get('success', False)
if not result['success']:
result['message'] = raw_response.get('message', 'Unknown error')
else:
# If we only had the set_id but not the name, look up the name
if not set_name and set_id in self.metadata_sets:
result['set_name'] = self.metadata_sets[set_id].get('name', '')
result['message'] = 'Metadata set successfully added to file object'
self._debug_print(f"Result: {result['message']}", result if self.debug else None)
return result
Parameters
| Name | Type | Default | Kind |
|---|---|---|---|
bases |
- | - |
Parameter Details
bases: Parameter of type
Return Value
Returns unspecified type
Class Interface
Methods
__init__(self, api_client, debug)
Purpose: Initialize the metadata catalog. Args: api_client: Authenticated FileCloudAPI client debug: Enable detailed debug output
Parameters:
api_client: Type: FileCloudAPIdebug: Type: bool
Returns: None
_debug_print(self, message, data)
Purpose: Print debug information if debug mode is enabled. Args: message: Debug message data: Optional data to print
Parameters:
message: Type: strdata: Type: Any
Returns: None
_ensure_initialized(self)
Purpose: Ensure the catalog is initialized.
Returns: None
initialize(self)
Purpose: Initialize the metadata catalog by loading all metadata sets and attributes. This version uses a single API call to get all metadata sets with their attributes.
Returns: None
_get_attribute_type_name(self, type_id)
Purpose: Map attribute type ID to a readable name.
Parameters:
type_id: Parameter
Returns: None
list_metadata_sets(self) -> List[Dict]
Purpose: List all available metadata sets. Returns: List[Dict]: List of metadata sets with their information
Returns: Returns List[Dict]
list_attributes(self, set_id) -> List[Dict]
Purpose: List all attributes in a specific metadata set. Args: set_id: ID of the metadata set Returns: List[Dict]: List of attributes with their information
Parameters:
set_id: Type: str
Returns: Returns List[Dict]
get_metadata_set_by_name(self, set_name) -> Optional[Dict]
Purpose: Get a metadata set by its name. Args: set_name: Name of the metadata set to find Returns: Optional[Dict]: Metadata set information or None if not found
Parameters:
set_name: Type: str
Returns: Returns Optional[Dict]
get_metadata_set_by_id(self, set_id) -> Optional[Dict]
Purpose: Get a metadata set by its ID. Args: set_id: ID of the metadata set to find Returns: Optional[Dict]: Metadata set information or None if not found
Parameters:
set_id: Type: str
Returns: Returns Optional[Dict]
get_attribute_by_name(self, set_id, attribute_name) -> Optional[Dict]
Purpose: Get a metadata attribute by its name within a specific set. Args: set_id: ID of the metadata set attribute_name: Name of the attribute to find Returns: Optional[Dict]: Attribute information or None if not found
Parameters:
set_id: Type: strattribute_name: Type: str
Returns: Returns Optional[Dict]
get_attribute_by_id(self, set_id, attribute_id) -> Optional[Dict]
Purpose: Get a metadata attribute by its ID within a specific set. Args: set_id: ID of the metadata set attribute_id: ID of the attribute to find Returns: Optional[Dict]: Attribute information or None if not found
Parameters:
set_id: Type: strattribute_id: Type: str
Returns: Returns Optional[Dict]
get_metadata_values(self, file_path) -> Dict
Purpose: Get metadata values for a specific file in a structured format. Supports multiple metadata sets per file. Args: file_path: Path to the file Returns: Dict: Structured metadata values with the following format: { 'success': bool, 'file_path': str, 'sets': [ { 'id': str, 'name': str, 'description': str, 'attributes': [ { 'id': str, 'name': str, 'description': str, 'type': str, 'value': str, 'required': bool, 'disabled': bool }, ... ] }, ... ], 'raw_response': dict # Original API response }
Parameters:
file_path: Type: str
Returns: Returns Dict
get_attribute_by_name_from_meta(self, metadata, name) -> Optional[Dict]
Purpose: Get an attribute from metadata results by its name. Args: metadata: Metadata dictionary returned by get_metadata_values() name: Name of the attribute to find Returns: Dict or None: The attribute dictionary or None if not found
Parameters:
metadata: Type: Dictname: Type: str
Returns: Returns Optional[Dict]
get_attributes_as_dict(self, metadata) -> Dict[str, Dict]
Purpose: Convert the attributes list to a dictionary keyed by attribute names. Args: metadata: Metadata dictionary returned by get_metadata_values() Returns: Dict: Dictionary of attributes with attribute names as keys
Parameters:
metadata: Type: Dict
Returns: Returns Dict[str, Dict]
get_attribute_values(self, metadata) -> Dict[str, str]
Purpose: Extract just the attribute names and values as a simple dictionary. Combines attributes from all metadata sets. Args: metadata: Metadata dictionary returned by get_metadata_values() Returns: Dict: Dictionary with attribute names as keys and their values as values
Parameters:
metadata: Type: Dict
Returns: Returns Dict[str, str]
save_attribute_values(self, file_path, metadata) -> Dict
Purpose: Save metadata values for a specific file using structured format. Args: file_path: Path to the file metadata: Structured metadata values in the format returned by get_metadata_values. Must contain at least 'set' with 'id' and 'attributes' list with 'id' and 'value'. { 'set': {'id': 'set_id'}, 'attributes': [ {'id': 'attr1_id', 'value': 'new_value1'}, {'id': 'attr2_id', 'value': 'new_value2'}, ... ] } Returns: Dict: Response from the API with additional structured information
Parameters:
file_path: Type: strmetadata: Type: Dict
Returns: Returns Dict
save_attribute_values_by_name(self, file_path, set_name, attributes) -> Dict
Purpose: Save metadata values using set name and attribute names instead of IDs. This is a convenience method that translates human-readable names to IDs and then calls save_attribute_values with the properly formatted input. Args: file_path: Path to the file set_name: Name of the metadata set attributes: Dictionary with attribute names as keys and their values as values { 'attr_name1': 'value1', 'attr_name2': 'value2', ... } Returns: Dict: Response from the API with additional structured information
Parameters:
file_path: Type: strset_name: Type: strattributes: Type: Dict[str, str]
Returns: Returns Dict
create_search_attributes(self, search_criteria) -> List[Dict]
Purpose: Create a list of search attributes formatted for the API, translating friendly names to IDs. Args: search_criteria: A list of dictionaries with keys 'set_name', 'attribute_name', and 'value' Returns: List[Dict]: Formatted attributes for search_metadata API call
Parameters:
search_criteria: Type: List[Dict]
Returns: Returns List[Dict]
search_files_by_metadata(self, search_criteria, search_string, search_location) -> List[str]
Purpose: Search for files that match specific metadata criteria. Args: search_criteria: A list of dictionaries with keys 'set_name', 'attribute_name', 'value', and optionally 'operator' search_string: Search string for filename pattern (use "**" for all files) search_location: Optional path to limit search to Returns: List[str]: List of file paths that match the criteria
Parameters:
search_criteria: Type: List[Dict]search_string: Type: strsearch_location: Type: Optional[str]
Returns: Returns List[str]
create_metadata_editor(self, file_path, metadata, views)
Purpose: Create an interactive panel for editing file metadata. This method creates a Holoviz Panel interface for viewing and editing metadata values. Once created, the panel can be displayed in a notebook or web application. Args: file_path: Optional path to the file whose metadata to edit. If provided and metadata is None, metadata will be loaded from this file. metadata: Optional pre-loaded metadata to edit. If provided, file_path is only used when saving changes. views: Optional list of views to display, choose from ['form', 'table', 'json'] Default is ['form'] if not specified Returns: panel.Column: Panel layout that can be displayed in notebooks or web apps
Parameters:
file_path: Type: strmetadata: Type: Dictviews: Type: List[str]
Returns: See docstring for return details
display_metadata_editor(self, file_path, metadata, views)
Purpose: Create and display an interactive metadata editor in a Jupyter notebook. Args: file_path: Optional path to the file whose metadata to edit metadata: Optional pre-loaded metadata to edit views: Optional list of views to display, choose from ['form', 'table', 'json'] Default is ['form'] if not specified Returns: panel.Column: The displayed panel object
Parameters:
file_path: Type: strmetadata: Type: Dictviews: Type: List[str]
Returns: See docstring for return details
debug_attribute_lookup(self, set_id, attribute_name) -> Dict
Purpose: Debug function to help diagnose attribute lookup issues. Args: set_id: ID of the metadata set attribute_name: Name of the attribute to find Returns: Dict: Debug information about the lookup
Parameters:
set_id: Type: strattribute_name: Type: str
Returns: Returns Dict
add_set_to_file_object(self, file_path, set_id, set_name) -> Dict
Purpose: Add a metadata set to a file or folder. Args: file_path: Path to the file or folder set_id: ID of the metadata set to add (either set_id or set_name must be provided) set_name: Name of the metadata set to add (used if set_id is not provided) Returns: Dict: Response with the following format: { 'success': bool, 'file_path': str, 'set_id': str, 'set_name': str, 'message': str, 'raw_response': dict # Original API response }
Parameters:
file_path: Type: strset_id: Type: strset_name: Type: str
Returns: Returns Dict
Required Imports
from typing import Dict
from typing import List
from typing import Optional
from typing import Any
from CDocs.utils.FC_api import FileCloudAPI
Usage Example
# Example usage:
# result = MetadataCatalog(bases)
Tags
Similar Components
AI-powered semantic similarity - components with related functionality:
-
class CorporateCatalogAppMetadata 59.2% similar
-
class CorporateCatalogAppMetadataCollection 56.5% similar
-
class FileCloudClient 55.0% similar
-
class FileCloudAPI 53.3% similar
-
class FileCloudAPI_v1 51.9% similar