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>
569 lines
23 KiB
Python
569 lines
23 KiB
Python
"""
|
|
CLI commands for user profile management.
|
|
|
|
This module provides command-line interface for user profile operations
|
|
including creation, modification, listing, and template integration.
|
|
"""
|
|
|
|
import click
|
|
import sys
|
|
import json
|
|
from typing import Optional, Dict, Any
|
|
from pathlib import Path
|
|
|
|
from .manager import ProfileManager, ProfileNotFoundError, ProfileValidationError
|
|
from .schema import ProfileSchema, ProfileData
|
|
|
|
|
|
@click.group(name='profile')
|
|
def profile_commands():
|
|
"""User profile management commands."""
|
|
pass
|
|
|
|
|
|
@profile_commands.command('create')
|
|
@click.argument('name')
|
|
@click.option('--description', help='Profile description')
|
|
@click.option('--first-name', help='First name')
|
|
@click.option('--last-name', help='Last name')
|
|
@click.option('--email', help='Email address')
|
|
@click.option('--phone', help='Phone number')
|
|
@click.option('--organization', help='Organization name')
|
|
@click.option('--position', help='Job position')
|
|
@click.option('--city', help='City')
|
|
@click.option('--country', help='Country')
|
|
@click.option('--interactive', '-i', is_flag=True, help='Interactive profile creation')
|
|
@click.option('--set-default', is_flag=True, help='Set as default profile')
|
|
@click.option('--database', 'db_path', help='Database path (defaults to config)')
|
|
def create_profile(name: str, description: Optional[str], first_name: Optional[str],
|
|
last_name: Optional[str], email: Optional[str], phone: Optional[str],
|
|
organization: Optional[str], position: Optional[str], city: Optional[str],
|
|
country: Optional[str], interactive: bool, set_default: bool,
|
|
db_path: Optional[str]):
|
|
"""Create a new user profile."""
|
|
try:
|
|
profile_manager = ProfileManager(db_path)
|
|
|
|
if interactive:
|
|
# Interactive profile creation
|
|
profile_data = _interactive_profile_creation()
|
|
if description is None:
|
|
description = click.prompt('Profile description', default='', show_default=False) or None
|
|
else:
|
|
# Command-line profile creation
|
|
profile_data = ProfileData(
|
|
first_name=first_name,
|
|
last_name=last_name,
|
|
contact=ProfileSchema.create_empty_profile().contact,
|
|
address=ProfileSchema.create_empty_profile().address,
|
|
organization=ProfileSchema.create_empty_profile().organization
|
|
)
|
|
|
|
# Set contact info
|
|
if email:
|
|
profile_data.contact.email = email
|
|
if phone:
|
|
profile_data.contact.phone = phone
|
|
|
|
# Set organization info
|
|
if organization:
|
|
profile_data.organization.name = organization
|
|
if position:
|
|
profile_data.organization.position = position
|
|
|
|
# Set address info
|
|
if city:
|
|
profile_data.address.city = city
|
|
if country:
|
|
profile_data.address.country = country
|
|
|
|
# Compute full name if first and last are provided
|
|
if first_name and last_name:
|
|
profile_data.full_name = f"{first_name} {last_name}"
|
|
|
|
# Create profile
|
|
profile_id = profile_manager.create_profile(
|
|
name=name,
|
|
data=profile_data,
|
|
description=description,
|
|
set_as_default=set_default
|
|
)
|
|
|
|
click.echo(f"✅ Created profile '{name}' (ID: {profile_id})")
|
|
if set_default:
|
|
click.echo(f"🎯 Set '{name}' as default profile")
|
|
|
|
except ValueError as e:
|
|
click.echo(f"Error: {e}", err=True)
|
|
sys.exit(1)
|
|
except ProfileValidationError as e:
|
|
click.echo(f"Validation Error: {e}", err=True)
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
click.echo(f"Error creating profile: {e}", err=True)
|
|
sys.exit(1)
|
|
|
|
|
|
@profile_commands.command('show')
|
|
@click.argument('name')
|
|
@click.option('--format', 'output_format', type=click.Choice(['json', 'yaml', 'table']),
|
|
default='table', help='Output format')
|
|
@click.option('--database', 'db_path', help='Database path (defaults to config)')
|
|
def show_profile(name: str, output_format: str, db_path: Optional[str]):
|
|
"""Show profile details."""
|
|
try:
|
|
profile_manager = ProfileManager(db_path)
|
|
profile_data = profile_manager.get_profile(name)
|
|
profile_info = profile_manager.get_profile_info(name)
|
|
|
|
if output_format == 'json':
|
|
output = {
|
|
'profile_info': profile_info,
|
|
'profile_data': profile_data.to_dict()
|
|
}
|
|
click.echo(json.dumps(output, indent=2, ensure_ascii=False))
|
|
elif output_format == 'yaml':
|
|
try:
|
|
import yaml
|
|
output = {
|
|
'profile_info': profile_info,
|
|
'profile_data': profile_data.to_dict()
|
|
}
|
|
click.echo(yaml.dump(output, default_flow_style=False, allow_unicode=True))
|
|
except ImportError:
|
|
click.echo("Error: PyYAML package required for YAML output", err=True)
|
|
sys.exit(1)
|
|
else:
|
|
# Table format
|
|
_display_profile_table(profile_info, profile_data)
|
|
|
|
except ProfileNotFoundError as e:
|
|
click.echo(f"Error: {e}", err=True)
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
click.echo(f"Error showing profile: {e}", err=True)
|
|
sys.exit(1)
|
|
|
|
|
|
@profile_commands.command('list')
|
|
@click.option('--include-inactive', is_flag=True, help='Include inactive profiles')
|
|
@click.option('--format', 'output_format', type=click.Choice(['table', 'json']),
|
|
default='table', help='Output format')
|
|
@click.option('--database', 'db_path', help='Database path (defaults to config)')
|
|
def list_profiles(include_inactive: bool, output_format: str, db_path: Optional[str]):
|
|
"""List all profiles."""
|
|
try:
|
|
profile_manager = ProfileManager(db_path)
|
|
profiles = profile_manager.list_profiles(include_inactive=include_inactive)
|
|
|
|
if not profiles:
|
|
click.echo("No profiles found.")
|
|
return
|
|
|
|
if output_format == 'json':
|
|
click.echo(json.dumps(profiles, indent=2, ensure_ascii=False))
|
|
else:
|
|
# Table format
|
|
click.echo("👤 User Profiles")
|
|
click.echo("=" * 80)
|
|
click.echo(f"{'Name':<20} {'Description':<30} {'Default':<8} {'Status':<8} {'Updated':<12}")
|
|
click.echo("-" * 80)
|
|
|
|
for profile in profiles:
|
|
status = "Active" if profile['is_active'] else "Inactive"
|
|
default = "Yes" if profile['is_default'] else "No"
|
|
description = profile['description'] or "(No description)"
|
|
updated = profile['updated_at'][:10] if profile['updated_at'] else "N/A"
|
|
|
|
click.echo(f"{profile['name']:<20} {description[:29]:<30} {default:<8} {status:<8} {updated:<12}")
|
|
|
|
click.echo(f"\nTotal: {len(profiles)} profiles")
|
|
if not include_inactive:
|
|
inactive_count = len([p for p in profiles if not p['is_active']])
|
|
if inactive_count > 0:
|
|
click.echo(f"💡 Use --include-inactive to show {inactive_count} inactive profiles")
|
|
|
|
except Exception as e:
|
|
click.echo(f"Error listing profiles: {e}", err=True)
|
|
sys.exit(1)
|
|
|
|
|
|
@profile_commands.command('update')
|
|
@click.argument('name')
|
|
@click.option('--description', help='Update profile description')
|
|
@click.option('--first-name', help='Update first name')
|
|
@click.option('--last-name', help='Update last name')
|
|
@click.option('--email', help='Update email address')
|
|
@click.option('--phone', help='Update phone number')
|
|
@click.option('--organization', help='Update organization name')
|
|
@click.option('--position', help='Update job position')
|
|
@click.option('--interactive', '-i', is_flag=True, help='Interactive profile update')
|
|
@click.option('--database', 'db_path', help='Database path (defaults to config)')
|
|
def update_profile(name: str, description: Optional[str], first_name: Optional[str],
|
|
last_name: Optional[str], email: Optional[str], phone: Optional[str],
|
|
organization: Optional[str], position: Optional[str], interactive: bool,
|
|
db_path: Optional[str]):
|
|
"""Update an existing profile."""
|
|
try:
|
|
profile_manager = ProfileManager(db_path)
|
|
|
|
if interactive:
|
|
# Interactive update
|
|
current_profile = profile_manager.get_profile(name)
|
|
profile_data = _interactive_profile_update(current_profile)
|
|
if description is None:
|
|
current_info = profile_manager.get_profile_info(name)
|
|
description = click.prompt('Profile description',
|
|
default=current_info['description'] or '',
|
|
show_default=True) or None
|
|
else:
|
|
# Get current profile and update specific fields
|
|
current_profile = profile_manager.get_profile(name)
|
|
|
|
# Update fields if provided
|
|
if first_name:
|
|
current_profile.first_name = first_name
|
|
if last_name:
|
|
current_profile.last_name = last_name
|
|
if email:
|
|
current_profile.contact.email = email
|
|
if phone:
|
|
current_profile.contact.phone = phone
|
|
if organization:
|
|
current_profile.organization.name = organization
|
|
if position:
|
|
current_profile.organization.position = position
|
|
|
|
# Recompute full name if first or last name changed
|
|
if first_name or last_name:
|
|
if current_profile.first_name and current_profile.last_name:
|
|
current_profile.full_name = f"{current_profile.first_name} {current_profile.last_name}"
|
|
|
|
profile_data = current_profile
|
|
|
|
# Update profile
|
|
success = profile_manager.update_profile(name, profile_data, description)
|
|
|
|
if success:
|
|
click.echo(f"✅ Updated profile '{name}'")
|
|
else:
|
|
click.echo(f"❌ Failed to update profile '{name}'")
|
|
sys.exit(1)
|
|
|
|
except ProfileNotFoundError as e:
|
|
click.echo(f"Error: {e}", err=True)
|
|
sys.exit(1)
|
|
except ProfileValidationError as e:
|
|
click.echo(f"Validation Error: {e}", err=True)
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
click.echo(f"Error updating profile: {e}", err=True)
|
|
sys.exit(1)
|
|
|
|
|
|
@profile_commands.command('delete')
|
|
@click.argument('name')
|
|
@click.option('--hard', is_flag=True, help='Permanently delete (default is soft delete)')
|
|
@click.option('--yes', is_flag=True, help='Skip confirmation prompt')
|
|
@click.option('--database', 'db_path', help='Database path (defaults to config)')
|
|
def delete_profile(name: str, hard: bool, yes: bool, db_path: Optional[str]):
|
|
"""Delete a profile."""
|
|
try:
|
|
profile_manager = ProfileManager(db_path)
|
|
|
|
# Confirm deletion unless --yes flag is used
|
|
if not yes:
|
|
action = "permanently delete" if hard else "deactivate"
|
|
if not click.confirm(f"Are you sure you want to {action} profile '{name}'?"):
|
|
click.echo("Deletion cancelled.")
|
|
return
|
|
|
|
success = profile_manager.delete_profile(name, hard_delete=hard)
|
|
|
|
if success:
|
|
action = "deleted permanently" if hard else "deactivated"
|
|
click.echo(f"✅ Profile '{name}' {action}")
|
|
else:
|
|
click.echo(f"❌ Failed to delete profile '{name}'")
|
|
sys.exit(1)
|
|
|
|
except ProfileNotFoundError as e:
|
|
click.echo(f"Error: {e}", err=True)
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
click.echo(f"Error deleting profile: {e}", err=True)
|
|
sys.exit(1)
|
|
|
|
|
|
@profile_commands.command('set-default')
|
|
@click.argument('name')
|
|
@click.option('--database', 'db_path', help='Database path (defaults to config)')
|
|
def set_default_profile(name: str, db_path: Optional[str]):
|
|
"""Set a profile as the default profile."""
|
|
try:
|
|
profile_manager = ProfileManager(db_path)
|
|
success = profile_manager.set_default_profile(name)
|
|
|
|
if success:
|
|
click.echo(f"✅ Set '{name}' as default profile")
|
|
else:
|
|
click.echo(f"❌ Failed to set '{name}' as default")
|
|
sys.exit(1)
|
|
|
|
except ProfileNotFoundError as e:
|
|
click.echo(f"Error: {e}", err=True)
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
click.echo(f"Error setting default: {e}", err=True)
|
|
sys.exit(1)
|
|
|
|
|
|
@profile_commands.command('export')
|
|
@click.argument('name')
|
|
@click.option('--format', 'output_format', type=click.Choice(['json', 'yaml']),
|
|
default='json', help='Export format')
|
|
@click.option('--output', 'output_file', help='Output file path')
|
|
@click.option('--database', 'db_path', help='Database path (defaults to config)')
|
|
def export_profile(name: str, output_format: str, output_file: Optional[str], db_path: Optional[str]):
|
|
"""Export profile to file or stdout."""
|
|
try:
|
|
profile_manager = ProfileManager(db_path)
|
|
exported_data = profile_manager.export_profile(name, output_format)
|
|
|
|
if output_file:
|
|
Path(output_file).write_text(exported_data, encoding='utf-8')
|
|
click.echo(f"✅ Exported profile '{name}' to {output_file}")
|
|
else:
|
|
click.echo(exported_data)
|
|
|
|
except ProfileNotFoundError as e:
|
|
click.echo(f"Error: {e}", err=True)
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
click.echo(f"Error exporting profile: {e}", err=True)
|
|
sys.exit(1)
|
|
|
|
|
|
@profile_commands.command('import')
|
|
@click.argument('name')
|
|
@click.argument('input_file')
|
|
@click.option('--format', 'input_format', type=click.Choice(['json', 'yaml']),
|
|
default='json', help='Import format')
|
|
@click.option('--overwrite', is_flag=True, help='Overwrite existing profile')
|
|
@click.option('--database', 'db_path', help='Database path (defaults to config)')
|
|
def import_profile(name: str, input_file: str, input_format: str, overwrite: bool, db_path: Optional[str]):
|
|
"""Import profile from file."""
|
|
try:
|
|
profile_manager = ProfileManager(db_path)
|
|
|
|
if not Path(input_file).exists():
|
|
click.echo(f"Error: Input file '{input_file}' not found", err=True)
|
|
sys.exit(1)
|
|
|
|
data = Path(input_file).read_text(encoding='utf-8')
|
|
profile_id = profile_manager.import_profile(name, data, input_format, overwrite)
|
|
|
|
action = "Updated" if overwrite else "Created"
|
|
click.echo(f"✅ {action} profile '{name}' (ID: {profile_id})")
|
|
|
|
except ValueError as e:
|
|
click.echo(f"Error: {e}", err=True)
|
|
sys.exit(1)
|
|
except ProfileValidationError as e:
|
|
click.echo(f"Validation Error: {e}", err=True)
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
click.echo(f"Error importing profile: {e}", err=True)
|
|
sys.exit(1)
|
|
|
|
|
|
@profile_commands.command('variables')
|
|
@click.option('--profile', help='Profile name (uses default if not specified)')
|
|
@click.option('--format', 'output_format', type=click.Choice(['table', 'json']),
|
|
default='table', help='Output format')
|
|
@click.option('--database', 'db_path', help='Database path (defaults to config)')
|
|
def show_template_variables(profile: Optional[str], output_format: str, db_path: Optional[str]):
|
|
"""Show template variables from a profile."""
|
|
try:
|
|
profile_manager = ProfileManager(db_path)
|
|
variables = profile_manager.get_template_variables(profile)
|
|
|
|
if not variables:
|
|
if profile:
|
|
click.echo(f"No template variables found for profile '{profile}'")
|
|
else:
|
|
click.echo("No default profile set or profile is empty")
|
|
return
|
|
|
|
profile_name = profile or "(default)"
|
|
|
|
if output_format == 'json':
|
|
click.echo(json.dumps(variables, indent=2, ensure_ascii=False))
|
|
else:
|
|
# Table format
|
|
click.echo(f"📋 Template Variables - {profile_name}")
|
|
click.echo("=" * 60)
|
|
click.echo(f"{'Variable':<25} {'Value':<35}")
|
|
click.echo("-" * 60)
|
|
|
|
for key, value in sorted(variables.items()):
|
|
value_str = str(value)[:34] if value else "(empty)"
|
|
click.echo(f"{key:<25} {value_str:<35}")
|
|
|
|
click.echo(f"\nTotal: {len(variables)} variables")
|
|
|
|
except ProfileNotFoundError as e:
|
|
click.echo(f"Error: {e}", err=True)
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
click.echo(f"Error showing variables: {e}", err=True)
|
|
sys.exit(1)
|
|
|
|
|
|
def _interactive_profile_creation() -> ProfileData:
|
|
"""Interactive profile creation helper."""
|
|
click.echo("📝 Interactive Profile Creation")
|
|
click.echo("=" * 40)
|
|
|
|
profile_data = ProfileSchema.create_empty_profile()
|
|
|
|
# Basic information
|
|
profile_data.first_name = click.prompt("First name", default="", show_default=False) or None
|
|
profile_data.last_name = click.prompt("Last name", default="", show_default=False) or None
|
|
|
|
if profile_data.first_name and profile_data.last_name:
|
|
default_full = f"{profile_data.first_name} {profile_data.last_name}"
|
|
profile_data.full_name = click.prompt("Full name", default=default_full) or None
|
|
|
|
profile_data.preferred_name = click.prompt("Preferred name", default="", show_default=False) or None
|
|
profile_data.title = click.prompt("Title (Mr/Ms/Dr/etc)", default="", show_default=False) or None
|
|
|
|
# Contact information
|
|
if click.confirm("Add contact information?", default=True):
|
|
profile_data.contact.email = click.prompt("Email", default="", show_default=False) or None
|
|
profile_data.contact.phone = click.prompt("Phone", default="", show_default=False) or None
|
|
profile_data.contact.website = click.prompt("Website", default="", show_default=False) or None
|
|
|
|
# Organization information
|
|
if click.confirm("Add organization information?", default=True):
|
|
profile_data.organization.name = click.prompt("Organization", default="", show_default=False) or None
|
|
profile_data.organization.position = click.prompt("Position/Title", default="", show_default=False) or None
|
|
profile_data.profession = click.prompt("Profession", default="", show_default=False) or None
|
|
|
|
# Address information
|
|
if click.confirm("Add address information?", default=False):
|
|
profile_data.address.city = click.prompt("City", default="", show_default=False) or None
|
|
profile_data.address.state = click.prompt("State/Province", default="", show_default=False) or None
|
|
profile_data.address.country = click.prompt("Country", default="", show_default=False) or None
|
|
|
|
return profile_data
|
|
|
|
|
|
def _interactive_profile_update(current_profile: ProfileData) -> ProfileData:
|
|
"""Interactive profile update helper."""
|
|
click.echo("📝 Interactive Profile Update")
|
|
click.echo("=" * 40)
|
|
click.echo("Current values shown in brackets. Press Enter to keep unchanged.")
|
|
|
|
# Basic information
|
|
current_profile.first_name = click.prompt(
|
|
"First name", default=current_profile.first_name or "", show_default=True
|
|
) or None
|
|
|
|
current_profile.last_name = click.prompt(
|
|
"Last name", default=current_profile.last_name or "", show_default=True
|
|
) or None
|
|
|
|
if current_profile.first_name and current_profile.last_name:
|
|
default_full = f"{current_profile.first_name} {current_profile.last_name}"
|
|
current_profile.full_name = click.prompt(
|
|
"Full name", default=current_profile.full_name or default_full, show_default=True
|
|
) or None
|
|
|
|
# Contact information
|
|
if click.confirm("Update contact information?", default=False):
|
|
current_profile.contact.email = click.prompt(
|
|
"Email", default=current_profile.contact.email or "", show_default=True
|
|
) or None
|
|
|
|
current_profile.contact.phone = click.prompt(
|
|
"Phone", default=current_profile.contact.phone or "", show_default=True
|
|
) or None
|
|
|
|
# Organization information
|
|
if click.confirm("Update organization information?", default=False):
|
|
current_profile.organization.name = click.prompt(
|
|
"Organization", default=current_profile.organization.name or "", show_default=True
|
|
) or None
|
|
|
|
current_profile.organization.position = click.prompt(
|
|
"Position/Title", default=current_profile.organization.position or "", show_default=True
|
|
) or None
|
|
|
|
return current_profile
|
|
|
|
|
|
def _display_profile_table(profile_info: Dict[str, Any], profile_data: ProfileData) -> None:
|
|
"""Display profile in table format."""
|
|
click.echo(f"👤 Profile: {profile_info['name']}")
|
|
click.echo("=" * 50)
|
|
|
|
if profile_info['description']:
|
|
click.echo(f"Description: {profile_info['description']}")
|
|
click.echo()
|
|
|
|
# Basic Information
|
|
click.echo("📋 Basic Information")
|
|
click.echo("-" * 20)
|
|
if profile_data.full_name:
|
|
click.echo(f"Full Name: {profile_data.full_name}")
|
|
if profile_data.first_name:
|
|
click.echo(f"First Name: {profile_data.first_name}")
|
|
if profile_data.last_name:
|
|
click.echo(f"Last Name: {profile_data.last_name}")
|
|
if profile_data.preferred_name:
|
|
click.echo(f"Preferred Name: {profile_data.preferred_name}")
|
|
if profile_data.title:
|
|
click.echo(f"Title: {profile_data.title}")
|
|
|
|
# Contact Information
|
|
if any([profile_data.contact.email, profile_data.contact.phone, profile_data.contact.website]):
|
|
click.echo(f"\n📞 Contact Information")
|
|
click.echo("-" * 20)
|
|
if profile_data.contact.email:
|
|
click.echo(f"Email: {profile_data.contact.email}")
|
|
if profile_data.contact.phone:
|
|
click.echo(f"Phone: {profile_data.contact.phone}")
|
|
if profile_data.contact.website:
|
|
click.echo(f"Website: {profile_data.contact.website}")
|
|
|
|
# Organization Information
|
|
if any([profile_data.organization.name, profile_data.organization.position, profile_data.profession]):
|
|
click.echo(f"\n🏢 Organization")
|
|
click.echo("-" * 15)
|
|
if profile_data.organization.name:
|
|
click.echo(f"Organization: {profile_data.organization.name}")
|
|
if profile_data.organization.position:
|
|
click.echo(f"Position: {profile_data.organization.position}")
|
|
if profile_data.profession:
|
|
click.echo(f"Profession: {profile_data.profession}")
|
|
|
|
# Address Information
|
|
if any([profile_data.address.city, profile_data.address.state, profile_data.address.country]):
|
|
click.echo(f"\n🌍 Address")
|
|
click.echo("-" * 10)
|
|
address_parts = []
|
|
if profile_data.address.city:
|
|
address_parts.append(profile_data.address.city)
|
|
if profile_data.address.state:
|
|
address_parts.append(profile_data.address.state)
|
|
if profile_data.address.country:
|
|
address_parts.append(profile_data.address.country)
|
|
click.echo(f"Location: {', '.join(address_parts)}")
|
|
|
|
# Metadata
|
|
click.echo(f"\n⏰ Metadata")
|
|
click.echo("-" * 10)
|
|
click.echo(f"Default Profile: {'Yes' if profile_info['is_default'] else 'No'}")
|
|
click.echo(f"Created: {profile_info['created_at'][:19] if profile_info['created_at'] else 'N/A'}")
|
|
click.echo(f"Updated: {profile_info['updated_at'][:19] if profile_info['updated_at'] else 'N/A'}") |