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>
355 lines
11 KiB
Python
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
|
|
) |