feat: implement comprehensive User Profile Management System (issue #107)
Complete user profile management system with CRUD operations and CLI integration: ## 🎯 Core Features Delivered - **ProfileManager**: Complete CRUD operations with database integration - **JSON Schema validation**: Comprehensive profile data validation - **Multiple profile support**: Named profiles (personal, work, etc.) - **Default profile system**: Set and manage default profiles - **Profile inheritance**: Merge profiles with override capabilities - **Template integration**: Extract flattened variables for template filling ## 📋 Profile Schema & Data Model - **Structured data classes**: ProfileData, ContactInfo, Address, Organization - **JSON Schema validation**: Full validation with field descriptions - **Flexible structure**: Support for nested data and custom fields - **Timestamp management**: Automatic created_at/updated_at tracking ## 🖥️ CLI Integration Complete - **9 CLI Commands**: create, show, list, update, delete, set-default, export, import, variables - **Multiple formats**: JSON, YAML, and table output formats - **Interactive mode**: Guided profile creation and updates - **Export/Import**: Full profile portability with validation - **Template variables**: Extract flattened variables for template systems ## 📊 Implementation Stats - **ProfileManager**: 500+ lines with comprehensive functionality - **ProfileSchema**: 350+ lines with validation and data structures - **CLI Commands**: 450+ lines of professional command interface - **Test Coverage**: 66 tests (36 core + 30 CLI) with 100% pass rate ## 🚀 **Ready for Template Integration** Foundation complete for Issue #99 (Auto Fill Templates) with: - Template variable extraction from profiles - Default profile system for seamless integration - Profile merging for complex template scenarios - Professional CLI for user profile management 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
663
markitect/profile/manager.py
Normal file
663
markitect/profile/manager.py
Normal file
@@ -0,0 +1,663 @@
|
||||
"""
|
||||
User Profile Manager for MarkiTect.
|
||||
|
||||
This module provides comprehensive CRUD operations for user profiles including
|
||||
database integration, validation, and profile inheritance functionality.
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional, List, Union
|
||||
from pathlib import Path
|
||||
from jsonschema import ValidationError
|
||||
|
||||
from .schema import ProfileSchema, ProfileData
|
||||
from ..database import DatabaseManager
|
||||
|
||||
|
||||
class ProfileNotFoundError(Exception):
|
||||
"""Raised when a profile is not found."""
|
||||
pass
|
||||
|
||||
|
||||
class ProfileValidationError(Exception):
|
||||
"""Raised when profile data fails validation."""
|
||||
pass
|
||||
|
||||
|
||||
class ProfileManager:
|
||||
"""
|
||||
Comprehensive user profile management system.
|
||||
|
||||
Handles CRUD operations, validation, and database integration for user profiles
|
||||
with support for multiple named profiles and template integration.
|
||||
"""
|
||||
|
||||
def __init__(self, db_path: Optional[str] = None):
|
||||
"""
|
||||
Initialize profile manager.
|
||||
|
||||
Args:
|
||||
db_path: Path to SQLite database file. If None, uses default from config.
|
||||
"""
|
||||
if db_path:
|
||||
self.db_path = db_path
|
||||
else:
|
||||
# Use default database path from MarkiTect
|
||||
from ..config_manager import ConfigurationManager
|
||||
config_manager = ConfigurationManager()
|
||||
config = config_manager.get_current_config()
|
||||
self.db_path = config.get('database_path', 'markitect.db')
|
||||
|
||||
self._ensure_database_initialized()
|
||||
|
||||
def _ensure_database_initialized(self) -> None:
|
||||
"""Ensure database and user_profiles table exist."""
|
||||
# Initialize main database if needed
|
||||
if not os.path.exists(self.db_path):
|
||||
db_manager = DatabaseManager(self.db_path)
|
||||
db_manager.initialize_database()
|
||||
|
||||
# Create user_profiles table
|
||||
self._create_profiles_table()
|
||||
|
||||
def _create_profiles_table(self) -> None:
|
||||
"""Create user_profiles table if it doesn't exist."""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS user_profiles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
data TEXT NOT NULL, -- JSON data
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
is_default BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
# Create index for faster lookups
|
||||
cursor.execute('''
|
||||
CREATE INDEX IF NOT EXISTS idx_user_profiles_name
|
||||
ON user_profiles(name)
|
||||
''')
|
||||
|
||||
cursor.execute('''
|
||||
CREATE INDEX IF NOT EXISTS idx_user_profiles_active
|
||||
ON user_profiles(is_active)
|
||||
''')
|
||||
|
||||
conn.commit()
|
||||
|
||||
except sqlite3.Error as e:
|
||||
conn.rollback()
|
||||
raise RuntimeError(f"Failed to create profiles table: {e}")
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def create_profile(self, name: str, data: Union[Dict[str, Any], ProfileData],
|
||||
description: Optional[str] = None,
|
||||
set_as_default: bool = False) -> int:
|
||||
"""
|
||||
Create a new user profile.
|
||||
|
||||
Args:
|
||||
name: Unique profile name
|
||||
data: Profile data (dict or ProfileData instance)
|
||||
description: Optional profile description
|
||||
set_as_default: Whether to set as default profile
|
||||
|
||||
Returns:
|
||||
ID of created profile
|
||||
|
||||
Raises:
|
||||
ProfileValidationError: If data is invalid
|
||||
ValueError: If profile name already exists
|
||||
"""
|
||||
# Convert ProfileData to dict if needed
|
||||
if isinstance(data, ProfileData):
|
||||
profile_data = data.to_dict()
|
||||
else:
|
||||
profile_data = data.copy()
|
||||
|
||||
# Add timestamps
|
||||
now = datetime.now().isoformat()
|
||||
profile_data.setdefault('created_at', now)
|
||||
profile_data['updated_at'] = now
|
||||
|
||||
# Validate data
|
||||
try:
|
||||
ProfileSchema.validate(profile_data)
|
||||
except ValidationError as e:
|
||||
raise ProfileValidationError(f"Profile data validation failed: {e.message}")
|
||||
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Check if profile name already exists
|
||||
cursor.execute('SELECT id FROM user_profiles WHERE name = ?', (name,))
|
||||
if cursor.fetchone():
|
||||
raise ValueError(f"Profile '{name}' already exists")
|
||||
|
||||
# If setting as default, unset any existing default
|
||||
if set_as_default:
|
||||
cursor.execute('''
|
||||
UPDATE user_profiles SET is_default = FALSE
|
||||
WHERE is_default = TRUE
|
||||
''')
|
||||
|
||||
# Insert new profile
|
||||
cursor.execute('''
|
||||
INSERT INTO user_profiles
|
||||
(name, description, data, is_default, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
''', (
|
||||
name,
|
||||
description,
|
||||
json.dumps(profile_data, indent=2),
|
||||
set_as_default,
|
||||
now,
|
||||
now
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
profile_id = cursor.lastrowid
|
||||
|
||||
return profile_id
|
||||
|
||||
except sqlite3.IntegrityError as e:
|
||||
conn.rollback()
|
||||
if "UNIQUE constraint failed" in str(e):
|
||||
raise ValueError(f"Profile '{name}' already exists")
|
||||
raise RuntimeError(f"Database error: {e}")
|
||||
|
||||
except sqlite3.Error as e:
|
||||
conn.rollback()
|
||||
raise RuntimeError(f"Failed to create profile: {e}")
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_profile(self, name: str) -> ProfileData:
|
||||
"""
|
||||
Get profile by name.
|
||||
|
||||
Args:
|
||||
name: Profile name
|
||||
|
||||
Returns:
|
||||
ProfileData instance
|
||||
|
||||
Raises:
|
||||
ProfileNotFoundError: If profile doesn't exist
|
||||
"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
cursor.execute('''
|
||||
SELECT data FROM user_profiles
|
||||
WHERE name = ? AND is_active = TRUE
|
||||
''', (name,))
|
||||
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
raise ProfileNotFoundError(f"Profile '{name}' not found")
|
||||
|
||||
profile_data = json.loads(row[0])
|
||||
return ProfileData.from_dict(profile_data)
|
||||
|
||||
except sqlite3.Error as e:
|
||||
raise RuntimeError(f"Failed to get profile: {e}")
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_profile_info(self, name: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get profile metadata without full data.
|
||||
|
||||
Args:
|
||||
name: Profile name
|
||||
|
||||
Returns:
|
||||
Dictionary with profile metadata
|
||||
|
||||
Raises:
|
||||
ProfileNotFoundError: If profile doesn't exist
|
||||
"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
cursor.execute('''
|
||||
SELECT id, name, description, is_active, is_default,
|
||||
created_at, updated_at
|
||||
FROM user_profiles
|
||||
WHERE name = ? AND is_active = TRUE
|
||||
''', (name,))
|
||||
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
raise ProfileNotFoundError(f"Profile '{name}' not found")
|
||||
|
||||
return {
|
||||
'id': row[0],
|
||||
'name': row[1],
|
||||
'description': row[2],
|
||||
'is_active': bool(row[3]),
|
||||
'is_default': bool(row[4]),
|
||||
'created_at': row[5],
|
||||
'updated_at': row[6]
|
||||
}
|
||||
|
||||
except sqlite3.Error as e:
|
||||
raise RuntimeError(f"Failed to get profile info: {e}")
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def update_profile(self, name: str, data: Union[Dict[str, Any], ProfileData],
|
||||
description: Optional[str] = None) -> bool:
|
||||
"""
|
||||
Update existing profile.
|
||||
|
||||
Args:
|
||||
name: Profile name
|
||||
data: Updated profile data
|
||||
description: Updated description (optional)
|
||||
|
||||
Returns:
|
||||
True if updated successfully
|
||||
|
||||
Raises:
|
||||
ProfileNotFoundError: If profile doesn't exist
|
||||
ProfileValidationError: If data is invalid
|
||||
"""
|
||||
# Convert ProfileData to dict if needed
|
||||
if isinstance(data, ProfileData):
|
||||
profile_data = data.to_dict()
|
||||
else:
|
||||
profile_data = data.copy()
|
||||
|
||||
# Update timestamp
|
||||
profile_data['updated_at'] = datetime.now().isoformat()
|
||||
|
||||
# Validate data
|
||||
try:
|
||||
ProfileSchema.validate(profile_data)
|
||||
except ValidationError as e:
|
||||
raise ProfileValidationError(f"Profile data validation failed: {e.message}")
|
||||
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Check if profile exists
|
||||
cursor.execute('SELECT id FROM user_profiles WHERE name = ? AND is_active = TRUE', (name,))
|
||||
if not cursor.fetchone():
|
||||
raise ProfileNotFoundError(f"Profile '{name}' not found")
|
||||
|
||||
# Update profile
|
||||
update_fields = ['data = ?', 'updated_at = ?']
|
||||
update_values = [json.dumps(profile_data, indent=2), profile_data['updated_at']]
|
||||
|
||||
if description is not None:
|
||||
update_fields.append('description = ?')
|
||||
update_values.append(description)
|
||||
|
||||
update_values.append(name) # For WHERE clause
|
||||
|
||||
cursor.execute(f'''
|
||||
UPDATE user_profiles
|
||||
SET {', '.join(update_fields)}
|
||||
WHERE name = ? AND is_active = TRUE
|
||||
''', update_values)
|
||||
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
except sqlite3.Error as e:
|
||||
conn.rollback()
|
||||
raise RuntimeError(f"Failed to update profile: {e}")
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def delete_profile(self, name: str, hard_delete: bool = False) -> bool:
|
||||
"""
|
||||
Delete a profile.
|
||||
|
||||
Args:
|
||||
name: Profile name
|
||||
hard_delete: If True, permanently delete. If False, mark as inactive.
|
||||
|
||||
Returns:
|
||||
True if deleted successfully
|
||||
|
||||
Raises:
|
||||
ProfileNotFoundError: If profile doesn't exist
|
||||
"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Check if profile exists
|
||||
cursor.execute('SELECT id FROM user_profiles WHERE name = ?', (name,))
|
||||
if not cursor.fetchone():
|
||||
raise ProfileNotFoundError(f"Profile '{name}' not found")
|
||||
|
||||
if hard_delete:
|
||||
# Permanently delete
|
||||
cursor.execute('DELETE FROM user_profiles WHERE name = ?', (name,))
|
||||
else:
|
||||
# Mark as inactive
|
||||
cursor.execute('''
|
||||
UPDATE user_profiles
|
||||
SET is_active = FALSE, updated_at = ?
|
||||
WHERE name = ?
|
||||
''', (datetime.now().isoformat(), name))
|
||||
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
except sqlite3.Error as e:
|
||||
conn.rollback()
|
||||
raise RuntimeError(f"Failed to delete profile: {e}")
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def list_profiles(self, include_inactive: bool = False) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
List all profiles.
|
||||
|
||||
Args:
|
||||
include_inactive: Whether to include inactive profiles
|
||||
|
||||
Returns:
|
||||
List of profile metadata dictionaries
|
||||
"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
if include_inactive:
|
||||
cursor.execute('''
|
||||
SELECT id, name, description, is_active, is_default,
|
||||
created_at, updated_at
|
||||
FROM user_profiles
|
||||
ORDER BY is_default DESC, name ASC
|
||||
''')
|
||||
else:
|
||||
cursor.execute('''
|
||||
SELECT id, name, description, is_active, is_default,
|
||||
created_at, updated_at
|
||||
FROM user_profiles
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY is_default DESC, name ASC
|
||||
''')
|
||||
|
||||
rows = cursor.fetchall()
|
||||
|
||||
return [
|
||||
{
|
||||
'id': row[0],
|
||||
'name': row[1],
|
||||
'description': row[2],
|
||||
'is_active': bool(row[3]),
|
||||
'is_default': bool(row[4]),
|
||||
'created_at': row[5],
|
||||
'updated_at': row[6]
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
except sqlite3.Error as e:
|
||||
raise RuntimeError(f"Failed to list profiles: {e}")
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def set_default_profile(self, name: str) -> bool:
|
||||
"""
|
||||
Set a profile as the default profile.
|
||||
|
||||
Args:
|
||||
name: Profile name
|
||||
|
||||
Returns:
|
||||
True if set successfully
|
||||
|
||||
Raises:
|
||||
ProfileNotFoundError: If profile doesn't exist
|
||||
"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Check if profile exists
|
||||
cursor.execute('SELECT id FROM user_profiles WHERE name = ? AND is_active = TRUE', (name,))
|
||||
if not cursor.fetchone():
|
||||
raise ProfileNotFoundError(f"Profile '{name}' not found")
|
||||
|
||||
# Unset all defaults
|
||||
cursor.execute('UPDATE user_profiles SET is_default = FALSE')
|
||||
|
||||
# Set new default
|
||||
cursor.execute('''
|
||||
UPDATE user_profiles
|
||||
SET is_default = TRUE, updated_at = ?
|
||||
WHERE name = ? AND is_active = TRUE
|
||||
''', (datetime.now().isoformat(), name))
|
||||
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
except sqlite3.Error as e:
|
||||
conn.rollback()
|
||||
raise RuntimeError(f"Failed to set default profile: {e}")
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_default_profile(self) -> Optional[ProfileData]:
|
||||
"""
|
||||
Get the default profile.
|
||||
|
||||
Returns:
|
||||
Default ProfileData or None if no default set
|
||||
"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
cursor.execute('''
|
||||
SELECT name, data FROM user_profiles
|
||||
WHERE is_default = TRUE AND is_active = TRUE
|
||||
LIMIT 1
|
||||
''')
|
||||
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
|
||||
profile_data = json.loads(row[1])
|
||||
return ProfileData.from_dict(profile_data)
|
||||
|
||||
except sqlite3.Error as e:
|
||||
raise RuntimeError(f"Failed to get default profile: {e}")
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def export_profile(self, name: str, format: str = 'json') -> str:
|
||||
"""
|
||||
Export profile to string format.
|
||||
|
||||
Args:
|
||||
name: Profile name
|
||||
format: Export format ('json' or 'yaml')
|
||||
|
||||
Returns:
|
||||
Exported profile string
|
||||
|
||||
Raises:
|
||||
ProfileNotFoundError: If profile doesn't exist
|
||||
ValueError: If format is unsupported
|
||||
"""
|
||||
profile_data = self.get_profile(name)
|
||||
profile_info = self.get_profile_info(name)
|
||||
|
||||
export_data = {
|
||||
'profile_info': profile_info,
|
||||
'profile_data': profile_data.to_dict()
|
||||
}
|
||||
|
||||
if format.lower() == 'json':
|
||||
return json.dumps(export_data, indent=2, ensure_ascii=False)
|
||||
elif format.lower() == 'yaml':
|
||||
try:
|
||||
import yaml
|
||||
return yaml.dump(export_data, default_flow_style=False, allow_unicode=True)
|
||||
except ImportError:
|
||||
raise ValueError("YAML format requires PyYAML package")
|
||||
else:
|
||||
raise ValueError(f"Unsupported export format: {format}")
|
||||
|
||||
def import_profile(self, name: str, data: str, format: str = 'json',
|
||||
overwrite: bool = False) -> int:
|
||||
"""
|
||||
Import profile from string format.
|
||||
|
||||
Args:
|
||||
name: New profile name (overrides name in data if different)
|
||||
data: Profile data string
|
||||
format: Import format ('json' or 'yaml')
|
||||
overwrite: Whether to overwrite existing profile
|
||||
|
||||
Returns:
|
||||
ID of imported profile
|
||||
|
||||
Raises:
|
||||
ValueError: If format is unsupported or profile exists without overwrite
|
||||
ProfileValidationError: If data is invalid
|
||||
"""
|
||||
if format.lower() == 'json':
|
||||
import_data = json.loads(data)
|
||||
elif format.lower() == 'yaml':
|
||||
try:
|
||||
import yaml
|
||||
import_data = yaml.safe_load(data)
|
||||
except ImportError:
|
||||
raise ValueError("YAML format requires PyYAML package")
|
||||
else:
|
||||
raise ValueError(f"Unsupported import format: {format}")
|
||||
|
||||
# Extract profile data
|
||||
if 'profile_data' in import_data:
|
||||
profile_data = import_data['profile_data']
|
||||
else:
|
||||
profile_data = import_data
|
||||
|
||||
description = None
|
||||
if 'profile_info' in import_data and 'description' in import_data['profile_info']:
|
||||
description = import_data['profile_info']['description']
|
||||
|
||||
# Check if profile exists
|
||||
try:
|
||||
existing_profile = self.get_profile_info(name)
|
||||
if not overwrite:
|
||||
raise ValueError(f"Profile '{name}' already exists. Use overwrite=True to replace.")
|
||||
# Update existing profile
|
||||
self.update_profile(name, profile_data, description)
|
||||
return existing_profile['id']
|
||||
except ProfileNotFoundError:
|
||||
# Create new profile
|
||||
return self.create_profile(name, profile_data, description)
|
||||
|
||||
def merge_profiles(self, base_profile: str, override_profile: str) -> ProfileData:
|
||||
"""
|
||||
Merge two profiles with override taking precedence.
|
||||
|
||||
Args:
|
||||
base_profile: Base profile name
|
||||
override_profile: Override profile name
|
||||
|
||||
Returns:
|
||||
Merged ProfileData
|
||||
|
||||
Raises:
|
||||
ProfileNotFoundError: If either profile doesn't exist
|
||||
"""
|
||||
base_data = self.get_profile(base_profile).to_dict()
|
||||
override_data = self.get_profile(override_profile).to_dict()
|
||||
|
||||
def deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Deep merge two dictionaries."""
|
||||
result = base.copy()
|
||||
for key, value in override.items():
|
||||
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
||||
result[key] = deep_merge(result[key], value)
|
||||
elif value is not None: # Only override with non-None values
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
merged_data = deep_merge(base_data, override_data)
|
||||
return ProfileData.from_dict(merged_data)
|
||||
|
||||
def get_template_variables(self, profile_name: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Get flattened template variables from a profile.
|
||||
|
||||
Args:
|
||||
profile_name: Profile name (uses default if None)
|
||||
|
||||
Returns:
|
||||
Flattened dictionary suitable for template filling
|
||||
|
||||
Raises:
|
||||
ProfileNotFoundError: If profile doesn't exist
|
||||
"""
|
||||
if profile_name:
|
||||
profile_data = self.get_profile(profile_name)
|
||||
else:
|
||||
profile_data = self.get_default_profile()
|
||||
if not profile_data:
|
||||
return {}
|
||||
|
||||
# Flatten nested structure for template variables
|
||||
variables = {}
|
||||
profile_dict = profile_data.to_dict()
|
||||
|
||||
def flatten_dict(d: Dict[str, Any], prefix: str = '') -> None:
|
||||
"""Recursively flatten dictionary."""
|
||||
for key, value in d.items():
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
new_key = f"{prefix}.{key}" if prefix else key
|
||||
|
||||
if isinstance(value, dict):
|
||||
flatten_dict(value, new_key)
|
||||
elif isinstance(value, (str, int, float, bool)):
|
||||
variables[new_key] = value
|
||||
elif isinstance(value, list):
|
||||
# Convert lists to comma-separated strings
|
||||
variables[new_key] = ', '.join(str(item) for item in value)
|
||||
|
||||
flatten_dict(profile_dict)
|
||||
|
||||
# Add some computed convenience variables
|
||||
if 'first_name' in variables and 'last_name' in variables:
|
||||
variables['full_name'] = f"{variables['first_name']} {variables['last_name']}"
|
||||
|
||||
return variables
|
||||
Reference in New Issue
Block a user