Files
tegwick b83dc14f7b 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>
2025-10-04 01:53:31 +02:00

355 lines
11 KiB
Python

"""
Profile schema definition and validation for MarkiTect user profiles.
This module defines the JSON schema structure for user profiles and provides
validation functionality to ensure profile data integrity.
"""
import json
from datetime import datetime
from typing import Dict, Any, Optional, List
from dataclasses import dataclass, asdict, field
from jsonschema import validate, ValidationError
@dataclass
class ContactInfo:
"""Contact information structure."""
email: Optional[str] = None
phone: Optional[str] = None
website: Optional[str] = None
linkedin: Optional[str] = None
github: Optional[str] = None
twitter: Optional[str] = None
@dataclass
class Address:
"""Address structure."""
street: Optional[str] = None
city: Optional[str] = None
state: Optional[str] = None
postal_code: Optional[str] = None
country: Optional[str] = None
@dataclass
class Organization:
"""Organization/company information."""
name: Optional[str] = None
position: Optional[str] = None
department: Optional[str] = None
website: Optional[str] = None
address: Optional[Address] = field(default_factory=Address)
@dataclass
class ProfileData:
"""
Complete user profile data structure.
This dataclass defines all the fields that can be stored in a user profile
for template auto-filling purposes.
"""
# Basic personal information
first_name: Optional[str] = None
last_name: Optional[str] = None
full_name: Optional[str] = None
preferred_name: Optional[str] = None
title: Optional[str] = None # Mr., Ms., Dr., etc.
# Contact information
contact: ContactInfo = field(default_factory=ContactInfo)
# Address information
address: Address = field(default_factory=Address)
# Professional information
organization: Organization = field(default_factory=Organization)
profession: Optional[str] = None
bio: Optional[str] = None
# Additional fields for template filling
custom_fields: Dict[str, Any] = field(default_factory=dict)
# Metadata
created_at: Optional[str] = None
updated_at: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
"""Convert profile data to dictionary."""
return asdict(self)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'ProfileData':
"""Create ProfileData from dictionary."""
# Handle nested structures
if 'contact' in data and isinstance(data['contact'], dict):
data['contact'] = ContactInfo(**data['contact'])
if 'address' in data and isinstance(data['address'], dict):
data['address'] = Address(**data['address'])
if 'organization' in data and isinstance(data['organization'], dict):
org_data = data['organization'].copy()
if 'address' in org_data and isinstance(org_data['address'], dict):
org_data['address'] = Address(**org_data['address'])
data['organization'] = Organization(**org_data)
return cls(**data)
class ProfileSchema:
"""
JSON Schema validation for user profiles.
Provides schema definition and validation methods to ensure
profile data integrity and consistency.
"""
# JSON Schema definition for profile validation
SCHEMA = {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "MarkiTect User Profile",
"type": "object",
"properties": {
"first_name": {
"type": ["string", "null"],
"maxLength": 100,
"description": "User's first name"
},
"last_name": {
"type": ["string", "null"],
"maxLength": 100,
"description": "User's last name"
},
"full_name": {
"type": ["string", "null"],
"maxLength": 200,
"description": "User's full name"
},
"preferred_name": {
"type": ["string", "null"],
"maxLength": 100,
"description": "User's preferred name"
},
"title": {
"type": ["string", "null"],
"maxLength": 20,
"description": "Title (Mr., Ms., Dr., etc.)"
},
"contact": {
"type": "object",
"properties": {
"email": {
"type": ["string", "null"],
"format": "email",
"description": "Email address"
},
"phone": {
"type": ["string", "null"],
"maxLength": 50,
"description": "Phone number"
},
"website": {
"type": ["string", "null"],
"format": "uri",
"description": "Personal website URL"
},
"linkedin": {
"type": ["string", "null"],
"description": "LinkedIn profile URL"
},
"github": {
"type": ["string", "null"],
"description": "GitHub profile URL"
},
"twitter": {
"type": ["string", "null"],
"description": "Twitter handle or URL"
}
},
"additionalProperties": False
},
"address": {
"type": "object",
"properties": {
"street": {
"type": ["string", "null"],
"maxLength": 200,
"description": "Street address"
},
"city": {
"type": ["string", "null"],
"maxLength": 100,
"description": "City"
},
"state": {
"type": ["string", "null"],
"maxLength": 100,
"description": "State or province"
},
"postal_code": {
"type": ["string", "null"],
"maxLength": 20,
"description": "Postal or ZIP code"
},
"country": {
"type": ["string", "null"],
"maxLength": 100,
"description": "Country"
}
},
"additionalProperties": False
},
"organization": {
"type": "object",
"properties": {
"name": {
"type": ["string", "null"],
"maxLength": 200,
"description": "Organization name"
},
"position": {
"type": ["string", "null"],
"maxLength": 100,
"description": "Job position/title"
},
"department": {
"type": ["string", "null"],
"maxLength": 100,
"description": "Department"
},
"website": {
"type": ["string", "null"],
"format": "uri",
"description": "Organization website"
},
"address": {
"$ref": "#/properties/address"
}
},
"additionalProperties": False
},
"profession": {
"type": ["string", "null"],
"maxLength": 100,
"description": "Professional role or occupation"
},
"bio": {
"type": ["string", "null"],
"maxLength": 2000,
"description": "Professional biography"
},
"custom_fields": {
"type": "object",
"description": "Additional custom fields for template filling",
"additionalProperties": True
},
"created_at": {
"type": ["string", "null"],
"format": "date-time",
"description": "Profile creation timestamp"
},
"updated_at": {
"type": ["string", "null"],
"format": "date-time",
"description": "Profile last update timestamp"
}
},
"additionalProperties": False
}
@classmethod
def validate(cls, data: Dict[str, Any]) -> None:
"""
Validate profile data against the schema.
Args:
data: Profile data dictionary to validate
Raises:
ValidationError: If data doesn't match schema
"""
validate(instance=data, schema=cls.SCHEMA)
@classmethod
def is_valid(cls, data: Dict[str, Any]) -> bool:
"""
Check if profile data is valid.
Args:
data: Profile data dictionary to check
Returns:
True if valid, False otherwise
"""
try:
cls.validate(data)
return True
except ValidationError:
return False
@classmethod
def get_field_description(cls, field_path: str) -> Optional[str]:
"""
Get description for a specific field.
Args:
field_path: Dot-separated field path (e.g., 'contact.email')
Returns:
Field description or None if not found
"""
parts = field_path.split('.')
current = cls.SCHEMA['properties']
try:
for part in parts:
if part in current:
current = current[part]
if 'properties' in current:
current = current['properties']
else:
return current.get('description')
return current.get('description')
except (KeyError, TypeError):
return None
@classmethod
def get_all_fields(cls) -> List[str]:
"""
Get list of all available field paths.
Returns:
List of dot-separated field paths
"""
fields = []
def extract_fields(schema_dict: Dict[str, Any], prefix: str = '') -> None:
if 'properties' not in schema_dict:
return
for field_name, field_def in schema_dict['properties'].items():
field_path = f"{prefix}.{field_name}" if prefix else field_name
fields.append(field_path)
if field_def.get('type') == 'object' and 'properties' in field_def:
extract_fields(field_def, field_path)
extract_fields(cls.SCHEMA)
return sorted(fields)
@classmethod
def create_empty_profile(self) -> ProfileData:
"""
Create an empty profile with default structure.
Returns:
Empty ProfileData instance
"""
now = datetime.now().isoformat()
return ProfileData(
created_at=now,
updated_at=now
)