alwrity chatbot assistant, content scheduler, and content repurposing

This commit is contained in:
ajaysi
2025-06-02 00:00:18 +05:30
parent 889021c078
commit 5ca2fd5977
69 changed files with 13952 additions and 3279 deletions

View File

@@ -0,0 +1,804 @@
# Alwrity Content Scheduler
A robust, reusable content scheduling system for Alwrity that integrates with existing features and provides comprehensive scheduling capabilities.
## Overview
The Content Scheduler is a standalone module that provides advanced scheduling capabilities for content publishing across multiple platforms. It uses APScheduler for reliable task scheduling and includes features for monitoring, error handling, and integration with existing Alwrity features.
## Features
### Core Scheduling Features
- [x] One-time content scheduling
- [x] Recurring content scheduling (cron-based)
- [x] Platform-specific scheduling
- [x] Batch scheduling
- [x] Schedule optimization based on platform analytics
- [x] Timezone support
- [x] Schedule conflict detection and resolution
### Monitoring & Management
- [x] Real-time schedule monitoring
- [x] Job status tracking (pending, running, completed, failed)
- [x] Failed job handling and retry mechanisms
- [x] Schedule health checks
- [x] Performance metrics and analytics
- [x] Schedule audit logs
### Integration Features
- [x] Seamless integration with Content Calendar
- Bidirectional sync with existing content calendar
- Real-time event synchronization
- Schedule-to-event conversion
- Calendar event management
- [x] Platform adapter system for different publishing platforms
- [ ] Webhook support for external integrations
- [ ] API endpoints for external access
- [ ] Event system for custom integrations
### Safety & Reliability
- [x] Persistent job storage
- [x] Automatic job recovery on system restart
- [x] Missed schedule detection and handling
- [x] Schedule validation and verification
- [x] Error handling and notification system
- [ ] Backup and restore capabilities
### User Interface
- [x] Interactive scheduling dashboard
- [x] Schedule visualization (calendar, timeline, list views)
- [x] Schedule management interface
- [x] Performance analytics dashboard
- [x] Schedule health monitoring
- [x] Alert and notification center
### Dashboard Capabilities
#### Overview Dashboard
- Real-time metrics display:
- Active schedules count
- Pending jobs count
- Completed jobs today
- Success rate percentage
- Upcoming schedules table with:
- Schedule title and content preview
- Platform information
- Scheduled time
- Current status
- Quick action buttons for common tasks
#### Schedule Management
- Create new schedules with:
- One-time or recurring options
- Multiple platform selection
- Content type specification
- Priority settings
- Advanced scheduling options
- Manage existing schedules:
- Edit schedule details
- Delete schedules
- Pause/Resume schedules
- Clone schedules
- Schedule visualization:
- Calendar view with color-coded status
- Timeline view for chronological display
- List view with sorting and filtering
- Drag-and-drop rescheduling
#### Job Monitor
- Real-time job status tracking:
- Pending jobs
- Running jobs
- Completed jobs
- Failed jobs
- Advanced filtering:
- By status
- By platform
- By date range
- By content type
- Job timeline visualization:
- Interactive timeline chart
- Job execution history
- Status changes tracking
- Detailed job information:
- Execution time
- Platform responses
- Error messages
- Retry attempts
#### Analytics Dashboard
- Performance metrics:
- Success rate trends
- Average execution time
- Error rate analysis
- Platform-specific metrics
- Content distribution:
- Platform-wise distribution
- Content type distribution
- Time-based distribution
- Schedule optimization insights:
- Best posting times
- Platform performance comparison
- Content type effectiveness
- Custom reports:
- Exportable metrics
- Custom date ranges
- Platform-specific reports
- Performance comparisons
#### Health Monitoring
- System health indicators:
- Scheduler status
- Database connection
- Platform connectivity
- Resource usage
- Alert system:
- Failed job notifications
- Schedule conflicts
- System warnings
- Performance alerts
- Health check history:
- Status changes
- Error logs
- Resolution tracking
- Maintenance records
#### User Experience Features
- Responsive design for all devices
- Dark/Light theme support
- Customizable dashboard layouts
- Keyboard shortcuts
- Bulk operations support
- Export/Import functionality
- Multi-language support
- Accessibility features
## Module Structure
```
lib/content_scheduler/
├── README.md
├── requirements.txt
├── core/
│ ├── __init__.py
│ ├── scheduler.py # Main scheduler implementation
│ ├── job_manager.py # Job management and persistence
│ ├── schedule_validator.py # Schedule validation and verification
│ ├── health_checker.py # Schedule health monitoring
│ ├── conflict_resolver.py # Schedule conflict detection and resolution
│ └── schedule_optimizer.py # Schedule optimization engine
├── models/
│ ├── __init__.py
│ ├── schedule.py # Schedule data models
│ ├── job.py # Job data models
│ └── platform.py # Platform-specific models
├── integrations/
│ ├── __init__.py
│ ├── platform_adapters/ # Platform-specific adapters
│ ├── calendar_integration.py # Content calendar integration
│ └── webhook_handler.py
├── ui/
│ ├── __init__.py
│ ├── dashboard.py # Main scheduling dashboard
│ ├── components/ # UI components
│ └── views/ # Different view implementations
├── utils/
│ ├── __init__.py
│ ├── date_utils.py
│ ├── error_handling.py
│ └── logging.py
└── tests/
├── __init__.py
├── test_scheduler.py
└── test_integrations.py
```
## Implementation Phases
### Phase 1: Core Scheduler ✅
- [x] Basic scheduler implementation with APScheduler
- [x] Job persistence and recovery
- [x] Basic error handling
- [x] Simple scheduling interface
### Phase 2: Integration & Platform Support ✅
- [x] Platform adapter system
- [x] Content Calendar integration
- [x] Basic monitoring system
- [x] Schedule validation
### Phase 3: Advanced Features ✅
- [x] Schedule optimization
- [x] Advanced error handling
- [x] Performance metrics
- [x] Health monitoring
### Phase 4: UI & Dashboard ✅
- [x] Interactive dashboard
- [x] Schedule visualization
- [x] Analytics dashboard
- [x] Alert system
## Technical Requirements
### Dependencies
- APScheduler >= 3.9.1
- SQLAlchemy (for job persistence)
- FastAPI (for API endpoints)
- Streamlit >= 1.24.0 (for dashboard)
- Pandas >= 1.5.0 (for data handling)
- Plotly >= 5.13.0 (for visualizations)
- Redis (optional, for distributed scheduling)
## Integration Points
### Content Calendar
- [x] Direct integration with existing calendar system
- [x] Bidirectional sync of schedules
- [x] Shared data models
- [x] Real-time event synchronization
- [x] Calendar event management
### Platform Adapters
- [x] Twitter
- [ ] Facebook
- [ ] LinkedIn
- [ ] Instagram
- [ ] WordPress
- [ ] Custom platform support
### External Systems
- [ ] Webhook support
- [ ] REST API
- [ ] Event system
- [ ] Notification system
## Monitoring & Maintenance
### Health Checks
- [x] Schedule validation
- [x] Job execution monitoring
- [x] System resource monitoring
- [x] Integration health checks
### Maintenance Tasks
- [ ] Log rotation
- [ ] Database cleanup
- [ ] Performance optimization
- [ ] Security updates
## Security Considerations
- [ ] API authentication
- [ ] Job execution security
- [ ] Data encryption
- [ ] Access control
- [ ] Audit logging
## Future Enhancements
### Short-term (Next Release)
- [ ] Webhook support for external integrations
- [ ] REST API endpoints
- [ ] Additional platform adapters
- [ ] Backup and restore capabilities
### Medium-term
- [ ] AI-powered schedule optimization
- Smart posting time recommendations
- Platform-specific optimal posting times
- Audience engagement pattern analysis
- Content type-specific timing optimization
- Content performance prediction
- Engagement rate forecasting
- Reach and visibility predictions
- Viral potential assessment
- Automated schedule adjustments
- Dynamic rescheduling based on performance
- A/B testing of posting times
- Real-time optimization based on engagement
- Audience behavior analysis
- Timezone-based audience activity patterns
- Content consumption patterns
- Engagement trend analysis
- Content type optimization
- Best content type for specific times
- Platform-specific content recommendations
- Content mix optimization
- [ ] Advanced analytics with ML insights
- Predictive analytics for content performance
- Audience growth forecasting
- Engagement trend analysis
- ROI prediction for scheduled content
- [ ] Multi-account support
- [ ] Custom scheduling algorithms
### Long-term
- [ ] Distributed scheduling support
- [ ] Advanced reporting system
- [ ] Machine learning for optimal posting times
- Deep learning models for engagement prediction
- Reinforcement learning for schedule optimization
- Natural language processing for content analysis
- Computer vision for visual content optimization
- [ ] Integration with external analytics tools
- [ ] AI-powered content recommendations
- Content type suggestions based on performance
- Topic and format recommendations
- Platform-specific content optimization
- Audience interest prediction
- [ ] Smart content repurposing
- Automated content adaptation for different platforms
- Format optimization based on platform performance
- Content refresh recommendations
- Cross-platform content strategy optimization
- [ ] Automated A/B testing framework
- Schedule timing experiments
- Content format testing
- Platform-specific optimization
- Audience segment testing
- [ ] Intelligent resource allocation
- Automated workload distribution
- Resource optimization based on content priority
- Smart queue management
- Performance-based resource allocation
### AI-Enhanced User Experience
- [ ] Smart scheduling assistant
- Natural language schedule creation
- Context-aware scheduling suggestions
- Automated conflict resolution
- Intelligent schedule adjustments
- [ ] Predictive maintenance
- System health forecasting
- Proactive issue detection
- Automated recovery suggestions
- Performance optimization recommendations
- [ ] Personalized dashboard
- AI-curated insights
- Custom metric recommendations
- Automated report generation
- Smart alert configuration
- [ ] Intelligent automation
- Smart schedule templates
- Automated content categorization
- Platform-specific optimization rules
- Dynamic workflow automation
- [ ] Advanced analytics visualization
- Interactive AI-powered insights
- Real-time performance predictions
- Trend analysis and forecasting
- Custom visualization recommendations
## Suggested Improvements & Enhancements
### Performance Optimizations
- [ ] Implement caching layer for frequently accessed data
- Schedule metadata caching
- Platform analytics caching
- Calendar event caching
- [ ] Optimize database queries
- Add database indexes for common queries
- Implement query result caching
- Optimize join operations
- [ ] Enhance job processing
- Implement job batching for similar tasks
- Add parallel processing for independent jobs
- Optimize resource allocation
### Reliability Enhancements
- [ ] Implement advanced error recovery
- Automatic retry with exponential backoff
- Circuit breaker pattern for external services
- Graceful degradation during failures
- [ ] Add comprehensive monitoring
- Real-time performance metrics
- Resource usage tracking
- Error rate monitoring
- [ ] Enhance data consistency
- Implement distributed transactions
- Add data validation layers
- Implement optimistic locking
### User Experience Improvements
#### Enhanced Dashboard Features
- [ ] Smart Dashboard Layout
- Drag-and-drop widget arrangement
- Customizable dashboard themes
- Responsive grid layout
- Collapsible sections
- Quick action toolbar
- Keyboard shortcuts support
- [ ] Advanced Content Management
- Bulk content scheduling
- Content templates library
- Content preview with platform simulation
- Content performance predictions
- Content recycling suggestions
- Content calendar sync status
- [ ] Intelligent Schedule Management
- Smart schedule suggestions
- Conflict-free scheduling
- Schedule templates
- Recurring schedule patterns
- Schedule optimization recommendations
- Schedule health indicators
- [ ] Platform-Specific Features
- Platform-specific scheduling rules
- Platform analytics integration
- Platform-specific content guidelines
- Platform performance metrics
- Platform-specific templates
- Platform health status
#### Improved Visualization
##### Interactive Calendar Views
- [ ] Multi-view Calendar System
- Day View
- Hour-by-hour schedule display
- Time slot availability indicators
- Schedule conflict highlighting
- Quick schedule creation
- Drag-and-drop rescheduling
- Schedule details on hover
- Week View
- 7-day calendar layout
- Daily schedule summaries
- Cross-day schedule visualization
- Week-over-week comparison
- Schedule density indicators
- Quick navigation controls
- Month View
- Full month calendar display
- Schedule count indicators
- Color-coded schedule types
- Month navigation
- Schedule preview on hover
- Bulk schedule management
- Year View
- Annual schedule overview
- Quarter-by-quarter breakdown
- Schedule distribution heatmap
- Year-over-year comparison
- Major milestone markers
- Schedule trend visualization
- [ ] Advanced Calendar Features
- Schedule Conflict Management
- Real-time conflict detection
- Visual conflict indicators
- Conflict resolution suggestions
- Automatic conflict avoidance
- Conflict history tracking
- Resolution workflow
- Calendar Overlay System
- Multiple calendar layers
- Platform-specific overlays
- Team schedule overlays
- Content type overlays
- Custom overlay creation
- Overlay visibility controls
- Interactive Controls
- Zoom and pan functionality
- Quick date navigation
- Schedule filtering
- View customization
- Export options
- Print layouts
##### Advanced Analytics Visualization
- [ ] Real-time Performance Charts
- Engagement Metrics
- Likes, shares, comments tracking
- Engagement rate trends
- Audience growth charts
- Platform-specific metrics
- Custom metric tracking
- Real-time updates
- Content Performance
- Content type effectiveness
- Best performing content
- Performance predictions
- A/B test results
- ROI visualization
- Trend analysis
- Platform Analytics
- Platform comparison charts
- Platform-specific metrics
- Cross-platform analysis
- Platform health indicators
- Performance benchmarks
- Growth tracking
- [ ] Custom Chart Builder
- Chart Types
- Line charts for trends
- Bar charts for comparisons
- Pie charts for distribution
- Scatter plots for correlation
- Heat maps for patterns
- Custom chart types
- Data Configuration
- Metric selection
- Time range control
- Data filtering
- Aggregation options
- Custom calculations
- Data export
- Visualization Options
- Color schemes
- Chart layouts
- Annotation tools
- Interactive elements
- Export formats
- Sharing options
##### Schedule Timeline Views
- [ ] Interactive Gantt Charts
- Schedule Visualization
- Task dependencies
- Progress tracking
- Milestone markers
- Resource allocation
- Timeline scaling
- Critical path highlighting
- Dependency Management
- Dependency creation
- Dependency visualization
- Conflict detection
- Resolution suggestions
- Impact analysis
- Dependency history
- Timeline Controls
- Zoom levels
- Pan navigation
- Filter options
- Group by options
- Export capabilities
- Print layouts
- [ ] Progress Tracking
- Visual Indicators
- Progress bars
- Status icons
- Completion percentages
- Delay indicators
- Risk markers
- Health status
- Milestone Tracking
- Milestone creation
- Due date tracking
- Completion status
- Dependency impact
- Notification triggers
- History tracking
##### Content Performance Dashboards
- [ ] Performance Scorecards
- Key Metrics
- Engagement rates
- Reach metrics
- Conversion rates
- ROI calculations
- Growth indicators
- Platform performance
- Custom Metrics
- Metric creation
- Formula builder
- Threshold setting
- Alert configuration
- Trend analysis
- Benchmark comparison
- [ ] ROI Visualization
- Financial Metrics
- Cost tracking
- Revenue attribution
- ROI calculations
- Budget allocation
- Cost efficiency
- Profitability analysis
- Performance Metrics
- Engagement value
- Conversion value
- Customer lifetime value
- Platform value
- Content value
- Campaign value
- [ ] Audience Insights
- Demographics
- Age distribution
- Gender breakdown
- Location data
- Device usage
- Platform preference
- Engagement patterns
- Behavior Analysis
- Content preferences
- Time patterns
- Platform usage
- Engagement trends
- Conversion paths
- Retention metrics
#### Better Notification System
- [ ] Smart Notification Center
- Centralized notification hub
- Notification categories
- Priority-based sorting
- Read/unread status
- Notification history
- Bulk notification actions
- [ ] Customizable Alert Rules
- Schedule status alerts
- Performance threshold alerts
- Platform-specific alerts
- Content engagement alerts
- System health alerts
- Custom alert conditions
- [ ] Multi-channel Notifications
- Email notifications
- In-app notifications
- Mobile push notifications
- SMS alerts
- Slack/Teams integration
- Webhook notifications
- [ ] Intelligent Notification Management
- Smart notification grouping
- Notification frequency control
- Quiet hours setting
- Do not disturb mode
- Notification preferences
- Notification templates
- [ ] Action-oriented Notifications
- One-click actions
- Quick response options
- Context-aware suggestions
- Batch action support
- Follow-up reminders
- Escalation paths
- [ ] Notification Analytics
- Notification engagement tracking
- Response time metrics
- Alert effectiveness analysis
- User preference insights
- Notification optimization
- Usage patterns
### Integration Enhancements
- [ ] Extended platform support
- Additional social media platforms
- Blog platforms integration
- Email marketing platforms
- Custom platform adapters
- [ ] Enhanced API capabilities
- GraphQL API support
- Webhook event system
- API rate limiting
- API versioning
- [ ] Advanced calendar features
- Multiple calendar support
- Calendar conflict resolution
- Calendar sharing and collaboration
- Calendar analytics
### Security Improvements
- [ ] Enhanced authentication
- OAuth 2.0 support
- Multi-factor authentication
- Role-based access control
- API key management
- [ ] Data protection
- End-to-end encryption
- Data masking
- Audit logging
- Compliance features
- [ ] Security monitoring
- Real-time security alerts
- Access pattern analysis
- Security audit reports
- Vulnerability scanning
### Scalability Enhancements
- [ ] Distributed architecture
- Horizontal scaling support
- Load balancing
- Service discovery
- Distributed caching
- [ ] High availability
- Multi-region deployment
- Automatic failover
- Data replication
- Disaster recovery
- [ ] Resource optimization
- Dynamic resource allocation
- Auto-scaling support
- Resource usage optimization
- Cost optimization
### Analytics & Insights
- [ ] Advanced analytics
- Predictive analytics
- Trend analysis
- Performance forecasting
- ROI tracking
- [ ] Custom reporting
- Report builder
- Custom metrics
- Export capabilities
- Scheduled reports
- [ ] Business intelligence
- KPI tracking
- Goal setting
- Performance benchmarking
- Competitive analysis
### Development & Maintenance
- [ ] Code quality improvements
- Enhanced test coverage
- Code documentation
- Performance profiling
- Code analysis tools
- [ ] Development workflow
- CI/CD pipeline
- Automated testing
- Code review process
- Release management
- [ ] Maintenance tools
- Automated backups
- Database maintenance
- System health checks
- Performance monitoring
### Future-Proofing
- [ ] Technology updates
- Framework upgrades
- Dependency updates
- Security patches
- Performance optimizations
- [ ] Feature extensibility
- Plugin system
- Custom integrations
- Extension points
- API evolution
- [ ] Innovation opportunities
- AI/ML integration
- Blockchain integration
- IoT integration
- Emerging technologies
## Contributing
Please read CONTRIBUTING.md for details on our code of conduct and the process for submitting pull requests.
## License
This project is licensed under the MIT License - see the LICENSE file for details.

View File

@@ -0,0 +1,403 @@
"""
Conflict resolution system for content scheduling.
"""
import logging
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional, Tuple
from dataclasses import dataclass
# Use unified database models
from lib.database.models import ContentItem, Schedule, ScheduleStatus
logger = logging.getLogger(__name__)
@dataclass
class ConflictInfo:
"""Information about a scheduling conflict."""
schedule_1: Schedule
schedule_2: Schedule
conflict_type: str
severity: str
description: str
suggested_resolution: str
class ConflictResolver:
"""Resolve scheduling conflicts automatically."""
def __init__(self):
"""Initialize the conflict resolver."""
self.logger = logger
self.resolution_strategies = {
'time_overlap': self._resolve_time_overlap,
'platform_conflict': self._resolve_platform_conflict,
'resource_conflict': self._resolve_resource_conflict,
'priority_conflict': self._resolve_priority_conflict
}
def detect_conflicts(self, schedules: List[Schedule]) -> List[ConflictInfo]:
"""Detect conflicts between schedules.
Args:
schedules: List of Schedule objects to check
Returns:
List of detected conflicts
"""
try:
conflicts = []
# Sort schedules by time
sorted_schedules = sorted(schedules, key=lambda x: x.scheduled_time)
for i in range(len(sorted_schedules)):
for j in range(i + 1, len(sorted_schedules)):
schedule_1 = sorted_schedules[i]
schedule_2 = sorted_schedules[j]
# Check for time overlap conflicts
time_conflicts = self._check_time_overlap(schedule_1, schedule_2)
conflicts.extend(time_conflicts)
# Check for platform conflicts
platform_conflicts = self._check_platform_conflict(schedule_1, schedule_2)
conflicts.extend(platform_conflicts)
# Check for priority conflicts
priority_conflicts = self._check_priority_conflict(schedule_1, schedule_2)
conflicts.extend(priority_conflicts)
return conflicts
except Exception as e:
self.logger.error(f"Error detecting conflicts: {str(e)}")
return []
def _check_time_overlap(self, schedule_1: Schedule, schedule_2: Schedule) -> List[ConflictInfo]:
"""Check for time overlap conflicts."""
conflicts = []
try:
# Assume each schedule takes 1 hour (can be made configurable)
duration = timedelta(hours=1)
end_1 = schedule_1.scheduled_time + duration
end_2 = schedule_2.scheduled_time + duration
# Check for overlap
if (schedule_1.scheduled_time < end_2 and end_1 > schedule_2.scheduled_time):
time_diff = abs((schedule_2.scheduled_time - schedule_1.scheduled_time).total_seconds() / 60)
severity = 'high' if time_diff < 30 else 'medium'
conflicts.append(ConflictInfo(
schedule_1=schedule_1,
schedule_2=schedule_2,
conflict_type='time_overlap',
severity=severity,
description=f"Schedules overlap by {60 - time_diff:.0f} minutes",
suggested_resolution=f"Move one schedule by at least {60 - time_diff + 15:.0f} minutes"
))
except Exception as e:
self.logger.error(f"Error checking time overlap: {str(e)}")
return conflicts
def _check_platform_conflict(self, schedule_1: Schedule, schedule_2: Schedule) -> List[ConflictInfo]:
"""Check for platform conflicts."""
conflicts = []
try:
# This is a placeholder - platform conflicts would depend on specific platform limitations
# For now, we'll check if schedules are too close on the same platform
time_diff = abs((schedule_2.scheduled_time - schedule_1.scheduled_time).total_seconds() / 60)
# If schedules are within 15 minutes, it might be a platform conflict
if time_diff < 15:
conflicts.append(ConflictInfo(
schedule_1=schedule_1,
schedule_2=schedule_2,
conflict_type='platform_conflict',
severity='medium',
description=f"Schedules too close for optimal platform performance",
suggested_resolution="Space schedules at least 15 minutes apart"
))
except Exception as e:
self.logger.error(f"Error checking platform conflict: {str(e)}")
return conflicts
def _check_priority_conflict(self, schedule_1: Schedule, schedule_2: Schedule) -> List[ConflictInfo]:
"""Check for priority conflicts."""
conflicts = []
try:
# Check if high priority items are scheduled too close to low priority items
if schedule_1.priority > 7 and schedule_2.priority < 4:
time_diff = abs((schedule_2.scheduled_time - schedule_1.scheduled_time).total_seconds() / 60)
if time_diff < 60: # Within 1 hour
conflicts.append(ConflictInfo(
schedule_1=schedule_1,
schedule_2=schedule_2,
conflict_type='priority_conflict',
severity='low',
description="High priority content scheduled close to low priority content",
suggested_resolution="Consider spacing high and low priority content further apart"
))
except Exception as e:
self.logger.error(f"Error checking priority conflict: {str(e)}")
return conflicts
def resolve_conflicts(self, conflicts: List[ConflictInfo]) -> Dict[str, Any]:
"""Resolve detected conflicts automatically.
Args:
conflicts: List of conflicts to resolve
Returns:
Dictionary containing resolution results
"""
try:
resolved_conflicts = []
unresolved_conflicts = []
schedule_adjustments = {}
for conflict in conflicts:
try:
# Get resolution strategy
strategy = self.resolution_strategies.get(conflict.conflict_type)
if strategy:
resolution = strategy(conflict)
if resolution['success']:
resolved_conflicts.append({
'conflict': conflict,
'resolution': resolution
})
# Track schedule adjustments
for schedule_id, adjustments in resolution.get('adjustments', {}).items():
if schedule_id not in schedule_adjustments:
schedule_adjustments[schedule_id] = {}
schedule_adjustments[schedule_id].update(adjustments)
else:
unresolved_conflicts.append(conflict)
else:
unresolved_conflicts.append(conflict)
except Exception as e:
self.logger.error(f"Error resolving conflict: {str(e)}")
unresolved_conflicts.append(conflict)
return {
'resolved_conflicts': resolved_conflicts,
'unresolved_conflicts': unresolved_conflicts,
'schedule_adjustments': schedule_adjustments,
'success_rate': len(resolved_conflicts) / len(conflicts) if conflicts else 1.0
}
except Exception as e:
self.logger.error(f"Error resolving conflicts: {str(e)}")
return {
'resolved_conflicts': [],
'unresolved_conflicts': conflicts,
'schedule_adjustments': {},
'success_rate': 0.0
}
def _resolve_time_overlap(self, conflict: ConflictInfo) -> Dict[str, Any]:
"""Resolve time overlap conflicts."""
try:
# Strategy: Move the lower priority schedule
schedule_1 = conflict.schedule_1
schedule_2 = conflict.schedule_2
# Determine which schedule to move
if schedule_1.priority >= schedule_2.priority:
schedule_to_move = schedule_2
anchor_schedule = schedule_1
else:
schedule_to_move = schedule_1
anchor_schedule = schedule_2
# Calculate new time (move 1.5 hours after anchor)
new_time = anchor_schedule.scheduled_time + timedelta(hours=1.5)
return {
'success': True,
'strategy': 'move_lower_priority',
'adjustments': {
str(schedule_to_move.id): {
'new_scheduled_time': new_time,
'reason': 'Resolved time overlap conflict'
}
},
'description': f"Moved schedule {schedule_to_move.id} to {new_time}"
}
except Exception as e:
self.logger.error(f"Error resolving time overlap: {str(e)}")
return {'success': False, 'error': str(e)}
def _resolve_platform_conflict(self, conflict: ConflictInfo) -> Dict[str, Any]:
"""Resolve platform conflicts."""
try:
# Strategy: Space schedules 20 minutes apart
schedule_1 = conflict.schedule_1
schedule_2 = conflict.schedule_2
# Move the later schedule
if schedule_1.scheduled_time < schedule_2.scheduled_time:
schedule_to_move = schedule_2
anchor_time = schedule_1.scheduled_time
else:
schedule_to_move = schedule_1
anchor_time = schedule_2.scheduled_time
new_time = anchor_time + timedelta(minutes=20)
return {
'success': True,
'strategy': 'space_schedules',
'adjustments': {
str(schedule_to_move.id): {
'new_scheduled_time': new_time,
'reason': 'Resolved platform conflict'
}
},
'description': f"Spaced schedule {schedule_to_move.id} to {new_time}"
}
except Exception as e:
self.logger.error(f"Error resolving platform conflict: {str(e)}")
return {'success': False, 'error': str(e)}
def _resolve_resource_conflict(self, conflict: ConflictInfo) -> Dict[str, Any]:
"""Resolve resource conflicts."""
try:
# This is a placeholder for resource conflict resolution
return {
'success': False,
'reason': 'Resource conflict resolution not implemented'
}
except Exception as e:
self.logger.error(f"Error resolving resource conflict: {str(e)}")
return {'success': False, 'error': str(e)}
def _resolve_priority_conflict(self, conflict: ConflictInfo) -> Dict[str, Any]:
"""Resolve priority conflicts."""
try:
# Strategy: Move low priority content away from high priority content
schedule_1 = conflict.schedule_1
schedule_2 = conflict.schedule_2
# Identify high and low priority schedules
if schedule_1.priority > schedule_2.priority:
high_priority = schedule_1
low_priority = schedule_2
else:
high_priority = schedule_2
low_priority = schedule_1
# Move low priority content 2 hours away
new_time = high_priority.scheduled_time + timedelta(hours=2)
return {
'success': True,
'strategy': 'separate_priorities',
'adjustments': {
str(low_priority.id): {
'new_scheduled_time': new_time,
'reason': 'Resolved priority conflict'
}
},
'description': f"Moved low priority schedule {low_priority.id} to {new_time}"
}
except Exception as e:
self.logger.error(f"Error resolving priority conflict: {str(e)}")
return {'success': False, 'error': str(e)}
def suggest_optimal_schedule(
self,
new_schedule: Schedule,
existing_schedules: List[Schedule]
) -> Dict[str, Any]:
"""Suggest optimal scheduling for new content.
Args:
new_schedule: New schedule to optimize
existing_schedules: List of existing schedules
Returns:
Dictionary containing optimization suggestions
"""
try:
suggestions = []
# Check for conflicts with proposed time
all_schedules = existing_schedules + [new_schedule]
conflicts = self.detect_conflicts(all_schedules)
if not conflicts:
return {
'optimal_time': new_schedule.scheduled_time,
'conflicts': [],
'suggestions': ['Current time is optimal']
}
# Generate alternative times
base_time = new_schedule.scheduled_time
alternative_times = []
# Try different time slots
for hours_offset in [1, 2, 3, -1, -2, -3]:
alt_time = base_time + timedelta(hours=hours_offset)
alt_schedule = Schedule(
content_item_id=new_schedule.content_item_id,
scheduled_time=alt_time,
status=new_schedule.status,
recurrence=new_schedule.recurrence,
priority=new_schedule.priority
)
# Check conflicts for this alternative
alt_conflicts = self.detect_conflicts(existing_schedules + [alt_schedule])
alternative_times.append({
'time': alt_time,
'conflicts': len(alt_conflicts),
'severity': max([c.severity for c in alt_conflicts], default='none')
})
# Sort by number of conflicts and severity
alternative_times.sort(key=lambda x: (x['conflicts'], x['severity']))
optimal_time = alternative_times[0]['time'] if alternative_times else new_schedule.scheduled_time
return {
'optimal_time': optimal_time,
'conflicts': conflicts,
'alternatives': alternative_times[:3], # Top 3 alternatives
'suggestions': [
f"Consider scheduling at {optimal_time}",
f"Current time has {len(conflicts)} conflicts",
"Review alternative times for better optimization"
]
}
except Exception as e:
self.logger.error(f"Error suggesting optimal schedule: {str(e)}")
return {
'optimal_time': new_schedule.scheduled_time,
'conflicts': [],
'suggestions': ['Error occurred during optimization']
}

View File

@@ -0,0 +1,584 @@
"""
Schedule health monitoring system.
"""
import logging
import asyncio
from typing import Dict, Any, List, Optional
from datetime import datetime, timedelta
from dataclasses import dataclass
from enum import Enum
from ..utils.error_handling import SchedulingError
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
class HealthStatus(Enum):
"""Health check status."""
HEALTHY = "healthy"
WARNING = "warning"
CRITICAL = "critical"
UNKNOWN = "unknown"
@dataclass
class HealthCheck:
"""Health check result."""
component: str
status: HealthStatus
message: str
details: Dict[str, Any]
timestamp: datetime
class ScheduleHealthChecker:
"""Schedule health monitoring system."""
def __init__(
self,
scheduler,
check_interval: int = 300, # 5 minutes
warning_threshold: int = 3,
critical_threshold: int = 5
):
"""Initialize the health checker.
Args:
scheduler: ContentScheduler instance
check_interval: Health check interval in seconds
warning_threshold: Number of failures before warning
critical_threshold: Number of failures before critical
"""
self.logger = logger
self.scheduler = scheduler
self.check_interval = check_interval
self.warning_threshold = warning_threshold
self.critical_threshold = critical_threshold
# Initialize health check history
self.health_history = []
# Initialize failure counters
self.failure_counts = {
'job_execution': 0,
'platform_publish': 0,
'schedule_conflicts': 0,
'resource_usage': 0
}
# Initialize monitoring task
self.monitoring_task = None
async def start_monitoring(self):
"""Start the health monitoring system."""
try:
if not self.monitoring_task:
self.monitoring_task = asyncio.create_task(self._monitor_health())
self.logger.info("Health monitoring started")
except Exception as e:
self.logger.error(f"Failed to start health monitoring: {str(e)}")
raise SchedulingError(f"Health monitoring start failed: {str(e)}")
async def stop_monitoring(self):
"""Stop the health monitoring system."""
try:
if self.monitoring_task:
self.monitoring_task.cancel()
self.monitoring_task = None
self.logger.info("Health monitoring stopped")
except Exception as e:
self.logger.error(f"Failed to stop health monitoring: {str(e)}")
raise SchedulingError(f"Health monitoring stop failed: {str(e)}")
async def _monitor_health(self):
"""Monitor system health periodically."""
while True:
try:
# Perform health checks
health_checks = await self._perform_health_checks()
# Update health history
self.health_history.extend(health_checks)
# Trim history if too long
if len(self.health_history) > 1000:
self.health_history = self.health_history[-1000:]
# Check for critical issues
critical_checks = [
check for check in health_checks
if check.status == HealthStatus.CRITICAL
]
if critical_checks:
await self._handle_critical_issues(critical_checks)
# Wait for next check
await asyncio.sleep(self.check_interval)
except asyncio.CancelledError:
break
except Exception as e:
self.logger.error(f"Health monitoring error: {str(e)}")
await asyncio.sleep(self.check_interval)
async def _perform_health_checks(self) -> List[HealthCheck]:
"""Perform all health checks.
Returns:
List of health check results
"""
checks = []
try:
# Check scheduler status
checks.append(await self._check_scheduler_status())
# Check job execution
checks.append(await self._check_job_execution())
# Check platform connectivity
checks.append(await self._check_platform_connectivity())
# Check resource usage
checks.append(await self._check_resource_usage())
# Check schedule conflicts
checks.append(await self._check_schedule_conflicts())
# Check database connection
checks.append(await self._check_database_connection())
# Check job store
checks.append(await self._check_job_store())
return checks
except Exception as e:
self.logger.error(f"Health check failed: {str(e)}")
return [
HealthCheck(
component="health_checker",
status=HealthStatus.CRITICAL,
message=f"Health check system error: {str(e)}",
details={'error': str(e)},
timestamp=datetime.utcnow()
)
]
async def _check_scheduler_status(self) -> HealthCheck:
"""Check scheduler status.
Returns:
Health check result
"""
try:
is_running = self.scheduler.scheduler.running
job_count = len(self.scheduler.scheduler.get_jobs())
if not is_running:
return HealthCheck(
component="scheduler",
status=HealthStatus.CRITICAL,
message="Scheduler is not running",
details={'job_count': job_count},
timestamp=datetime.utcnow()
)
return HealthCheck(
component="scheduler",
status=HealthStatus.HEALTHY,
message="Scheduler is running",
details={'job_count': job_count},
timestamp=datetime.utcnow()
)
except Exception as e:
return HealthCheck(
component="scheduler",
status=HealthStatus.CRITICAL,
message=f"Scheduler check failed: {str(e)}",
details={'error': str(e)},
timestamp=datetime.utcnow()
)
async def _check_job_execution(self) -> HealthCheck:
"""Check job execution health.
Returns:
Health check result
"""
try:
# Get recent job history
recent_jobs = [
job for job in self.scheduler.job_status.values()
if datetime.utcnow() - job['created_at'] < timedelta(hours=24)
]
# Calculate failure rate
total_jobs = len(recent_jobs)
failed_jobs = len([
job for job in recent_jobs
if job['status'] == 'FAILED'
])
failure_rate = failed_jobs / total_jobs if total_jobs > 0 else 0
# Update failure counter
self.failure_counts['job_execution'] = failed_jobs
if failure_rate >= 0.2: # 20% failure rate
return HealthCheck(
component="job_execution",
status=HealthStatus.CRITICAL,
message="High job failure rate detected",
details={
'total_jobs': total_jobs,
'failed_jobs': failed_jobs,
'failure_rate': failure_rate
},
timestamp=datetime.utcnow()
)
elif failure_rate >= 0.1: # 10% failure rate
return HealthCheck(
component="job_execution",
status=HealthStatus.WARNING,
message="Elevated job failure rate",
details={
'total_jobs': total_jobs,
'failed_jobs': failed_jobs,
'failure_rate': failure_rate
},
timestamp=datetime.utcnow()
)
return HealthCheck(
component="job_execution",
status=HealthStatus.HEALTHY,
message="Job execution is healthy",
details={
'total_jobs': total_jobs,
'failed_jobs': failed_jobs,
'failure_rate': failure_rate
},
timestamp=datetime.utcnow()
)
except Exception as e:
return HealthCheck(
component="job_execution",
status=HealthStatus.CRITICAL,
message=f"Job execution check failed: {str(e)}",
details={'error': str(e)},
timestamp=datetime.utcnow()
)
async def _check_platform_connectivity(self) -> HealthCheck:
"""Check platform connectivity.
Returns:
Health check result
"""
try:
# Get unique platforms from recent jobs
platforms = set()
for job in self.scheduler.job_status.values():
if 'schedule' in job:
platforms.update(job['schedule'].platforms)
# Check each platform
platform_status = {}
for platform in platforms:
try:
adapter = self.scheduler._get_platform_adapter(platform)
# Try to get platform status
status = await adapter.get_platform_status()
platform_status[platform] = status['status']
except Exception as e:
platform_status[platform] = 'error'
self.failure_counts['platform_publish'] += 1
# Check overall status
if any(status == 'error' for status in platform_status.values()):
return HealthCheck(
component="platform_connectivity",
status=HealthStatus.CRITICAL,
message="Platform connectivity issues detected",
details={'platform_status': platform_status},
timestamp=datetime.utcnow()
)
return HealthCheck(
component="platform_connectivity",
status=HealthStatus.HEALTHY,
message="Platform connectivity is healthy",
details={'platform_status': platform_status},
timestamp=datetime.utcnow()
)
except Exception as e:
return HealthCheck(
component="platform_connectivity",
status=HealthStatus.CRITICAL,
message=f"Platform connectivity check failed: {str(e)}",
details={'error': str(e)},
timestamp=datetime.utcnow()
)
async def _check_resource_usage(self) -> HealthCheck:
"""Check system resource usage.
Returns:
Health check result
"""
try:
import psutil
# Get system metrics
cpu_percent = psutil.cpu_percent()
memory_percent = psutil.virtual_memory().percent
disk_percent = psutil.disk_usage('/').percent
# Check thresholds
if cpu_percent > 90 or memory_percent > 90 or disk_percent > 90:
self.failure_counts['resource_usage'] += 1
return HealthCheck(
component="resource_usage",
status=HealthStatus.CRITICAL,
message="High resource usage detected",
details={
'cpu_percent': cpu_percent,
'memory_percent': memory_percent,
'disk_percent': disk_percent
},
timestamp=datetime.utcnow()
)
elif cpu_percent > 70 or memory_percent > 70 or disk_percent > 70:
return HealthCheck(
component="resource_usage",
status=HealthStatus.WARNING,
message="Elevated resource usage",
details={
'cpu_percent': cpu_percent,
'memory_percent': memory_percent,
'disk_percent': disk_percent
},
timestamp=datetime.utcnow()
)
return HealthCheck(
component="resource_usage",
status=HealthStatus.HEALTHY,
message="Resource usage is healthy",
details={
'cpu_percent': cpu_percent,
'memory_percent': memory_percent,
'disk_percent': disk_percent
},
timestamp=datetime.utcnow()
)
except Exception as e:
return HealthCheck(
component="resource_usage",
status=HealthStatus.CRITICAL,
message=f"Resource usage check failed: {str(e)}",
details={'error': str(e)},
timestamp=datetime.utcnow()
)
async def _check_schedule_conflicts(self) -> HealthCheck:
"""Check for schedule conflicts.
Returns:
Health check result
"""
try:
# Get all pending schedules
pending_schedules = [
job['schedule'] for job in self.scheduler.job_status.values()
if job['status'] == 'PENDING'
]
# Check for conflicts
conflicts = await self.scheduler.conflict_resolver.detect_conflicts(
pending_schedules
)
if conflicts:
self.failure_counts['schedule_conflicts'] += len(conflicts)
return HealthCheck(
component="schedule_conflicts",
status=HealthStatus.WARNING,
message="Schedule conflicts detected",
details={
'conflict_count': len(conflicts),
'conflicts': [c.dict() for c in conflicts]
},
timestamp=datetime.utcnow()
)
return HealthCheck(
component="schedule_conflicts",
status=HealthStatus.HEALTHY,
message="No schedule conflicts detected",
details={'conflict_count': 0},
timestamp=datetime.utcnow()
)
except Exception as e:
return HealthCheck(
component="schedule_conflicts",
status=HealthStatus.CRITICAL,
message=f"Schedule conflict check failed: {str(e)}",
details={'error': str(e)},
timestamp=datetime.utcnow()
)
async def _check_database_connection(self) -> HealthCheck:
"""Check database connection health.
Returns:
Health check result
"""
try:
session = self.scheduler.Session()
session.execute("SELECT 1")
session.close()
return HealthCheck(
component="database",
status=HealthStatus.HEALTHY,
message="Database connection is healthy",
details={},
timestamp=datetime.utcnow()
)
except Exception as e:
return HealthCheck(
component="database",
status=HealthStatus.CRITICAL,
message=f"Database connection failed: {str(e)}",
details={'error': str(e)},
timestamp=datetime.utcnow()
)
async def _check_job_store(self) -> HealthCheck:
"""Check job store health.
Returns:
Health check result
"""
try:
# Get job store statistics
job_count = len(self.scheduler.scheduler.get_jobs())
store_size = len(self.scheduler.job_status)
if job_count != store_size:
return HealthCheck(
component="job_store",
status=HealthStatus.WARNING,
message="Job store inconsistency detected",
details={
'job_count': job_count,
'store_size': store_size
},
timestamp=datetime.utcnow()
)
return HealthCheck(
component="job_store",
status=HealthStatus.HEALTHY,
message="Job store is healthy",
details={
'job_count': job_count,
'store_size': store_size
},
timestamp=datetime.utcnow()
)
except Exception as e:
return HealthCheck(
component="job_store",
status=HealthStatus.CRITICAL,
message=f"Job store check failed: {str(e)}",
details={'error': str(e)},
timestamp=datetime.utcnow()
)
async def _handle_critical_issues(self, critical_checks: List[HealthCheck]):
"""Handle critical health issues.
Args:
critical_checks: List of critical health checks
"""
try:
# Log critical issues
for check in critical_checks:
self.logger.error(
f"Critical health issue in {check.component}: {check.message}"
)
# Attempt recovery actions
for check in critical_checks:
if check.component == "scheduler" and not self.scheduler.scheduler.running:
await self.scheduler.start()
elif check.component == "database":
# Attempt to reconnect
self.scheduler.engine.dispose()
self.scheduler.engine = create_engine(self.scheduler.db_url)
self.scheduler.Session = sessionmaker(bind=self.scheduler.engine)
elif check.component == "job_store":
# Attempt to recover job store
await self.scheduler._recover_jobs()
# Reset failure counters if recovery successful
self.failure_counts = {k: 0 for k in self.failure_counts}
except Exception as e:
self.logger.error(f"Failed to handle critical issues: {str(e)}")
def get_health_summary(self) -> Dict[str, Any]:
"""Get health check summary.
Returns:
Dictionary containing health summary
"""
try:
# Get latest health checks
latest_checks = {
check.component: check
for check in self.health_history[-len(self.health_history):]
}
# Calculate overall status
if any(check.status == HealthStatus.CRITICAL for check in latest_checks.values()):
overall_status = HealthStatus.CRITICAL
elif any(check.status == HealthStatus.WARNING for check in latest_checks.values()):
overall_status = HealthStatus.WARNING
else:
overall_status = HealthStatus.HEALTHY
return {
'status': overall_status.value,
'components': {
component: {
'status': check.status.value,
'message': check.message,
'details': check.details,
'timestamp': check.timestamp.isoformat()
}
for component, check in latest_checks.items()
},
'failure_counts': self.failure_counts,
'last_check': datetime.utcnow().isoformat()
}
except Exception as e:
self.logger.error(f"Failed to get health summary: {str(e)}")
return {
'status': HealthStatus.UNKNOWN.value,
'error': str(e),
'last_check': datetime.utcnow().isoformat()
}

View File

@@ -0,0 +1,597 @@
"""
Schedule optimization system for content scheduling.
"""
import logging
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional, Tuple
from dataclasses import dataclass
import numpy as np
from collections import defaultdict
# Use unified database models
from lib.database.models import ContentItem, Schedule, ScheduleStatus, ContentType, Platform, get_session
logger = logging.getLogger(__name__)
@dataclass
class OptimizationResult:
"""Result of schedule optimization."""
original_schedule: Schedule
optimized_time: datetime
improvement_score: float
optimization_reason: str
confidence: float
class ScheduleOptimizer:
"""Optimize content scheduling for maximum engagement."""
def __init__(self):
"""Initialize the schedule optimizer."""
self.logger = logger
self.session = get_session()
# Platform-specific optimal times (can be made configurable)
self.platform_optimal_times = {
Platform.TWITTER: [9, 12, 15, 18], # Hours of day
Platform.FACEBOOK: [9, 13, 15],
Platform.LINKEDIN: [8, 12, 17],
Platform.INSTAGRAM: [11, 14, 17, 19],
Platform.YOUTUBE: [14, 16, 18, 20]
}
# Content type engagement patterns
self.content_type_patterns = {
ContentType.ARTICLE: {'peak_hours': [9, 14, 16], 'duration': 2},
ContentType.VIDEO: {'peak_hours': [12, 18, 20], 'duration': 3},
ContentType.IMAGE: {'peak_hours': [11, 15, 19], 'duration': 1},
ContentType.SOCIAL_POST: {'peak_hours': [8, 12, 17, 21], 'duration': 1}
}
def optimize_schedule(self, schedule: Schedule) -> OptimizationResult:
"""Optimize a single schedule for better engagement.
Args:
schedule: Schedule to optimize
Returns:
OptimizationResult with optimization details
"""
try:
# Get content item details
content_item = self.session.query(ContentItem).filter(
ContentItem.id == schedule.content_item_id
).first()
if not content_item:
return OptimizationResult(
original_schedule=schedule,
optimized_time=schedule.scheduled_time,
improvement_score=0.0,
optimization_reason="Content item not found",
confidence=0.0
)
# Calculate current engagement score
current_score = self._calculate_engagement_score(
schedule.scheduled_time,
content_item.content_type,
schedule.priority
)
# Find optimal time
optimal_time, optimal_score = self._find_optimal_time(
schedule,
content_item
)
# Calculate improvement
improvement_score = optimal_score - current_score
confidence = min(improvement_score / current_score, 1.0) if current_score > 0 else 0.0
# Generate optimization reason
reason = self._generate_optimization_reason(
schedule.scheduled_time,
optimal_time,
content_item.content_type,
improvement_score
)
return OptimizationResult(
original_schedule=schedule,
optimized_time=optimal_time,
improvement_score=improvement_score,
optimization_reason=reason,
confidence=confidence
)
except Exception as e:
self.logger.error(f"Error optimizing schedule: {str(e)}")
return OptimizationResult(
original_schedule=schedule,
optimized_time=schedule.scheduled_time,
improvement_score=0.0,
optimization_reason=f"Optimization error: {str(e)}",
confidence=0.0
)
def optimize_multiple_schedules(
self,
schedules: List[Schedule],
avoid_conflicts: bool = True
) -> List[OptimizationResult]:
"""Optimize multiple schedules considering conflicts.
Args:
schedules: List of schedules to optimize
avoid_conflicts: Whether to avoid scheduling conflicts
Returns:
List of optimization results
"""
try:
results = []
optimized_times = []
# Sort schedules by priority (high priority first)
sorted_schedules = sorted(schedules, key=lambda x: x.priority, reverse=True)
for schedule in sorted_schedules:
# Optimize individual schedule
result = self.optimize_schedule(schedule)
if avoid_conflicts:
# Check for conflicts with already optimized schedules
conflict_free_time = self._find_conflict_free_time(
result.optimized_time,
optimized_times,
schedule
)
if conflict_free_time != result.optimized_time:
# Recalculate scores for conflict-free time
content_item = self.session.query(ContentItem).filter(
ContentItem.id == schedule.content_item_id
).first()
if content_item:
new_score = self._calculate_engagement_score(
conflict_free_time,
content_item.content_type,
schedule.priority
)
original_score = self._calculate_engagement_score(
schedule.scheduled_time,
content_item.content_type,
schedule.priority
)
result.optimized_time = conflict_free_time
result.improvement_score = new_score - original_score
result.optimization_reason += " (adjusted to avoid conflicts)"
results.append(result)
optimized_times.append(result.optimized_time)
return results
except Exception as e:
self.logger.error(f"Error optimizing multiple schedules: {str(e)}")
return []
def suggest_optimal_times(
self,
content_type: ContentType,
date_range: Tuple[datetime, datetime],
count: int = 5
) -> List[Dict[str, Any]]:
"""Suggest optimal times for new content.
Args:
content_type: Type of content to schedule
date_range: Date range to consider
count: Number of suggestions to return
Returns:
List of suggested optimal times with scores
"""
try:
suggestions = []
start_date, end_date = date_range
# Generate candidate times
current_date = start_date
while current_date <= end_date:
# Get optimal hours for this content type
if content_type in self.content_type_patterns:
optimal_hours = self.content_type_patterns[content_type]['peak_hours']
else:
optimal_hours = [9, 12, 15, 18] # Default hours
for hour in optimal_hours:
candidate_time = current_date.replace(
hour=hour,
minute=0,
second=0,
microsecond=0
)
if start_date <= candidate_time <= end_date:
score = self._calculate_engagement_score(
candidate_time,
content_type,
priority=5 # Default priority
)
suggestions.append({
'time': candidate_time,
'score': score,
'day_of_week': candidate_time.strftime('%A'),
'hour': hour,
'reason': self._get_time_suggestion_reason(candidate_time, content_type)
})
current_date += timedelta(days=1)
# Sort by score and return top suggestions
suggestions.sort(key=lambda x: x['score'], reverse=True)
return suggestions[:count]
except Exception as e:
self.logger.error(f"Error suggesting optimal times: {str(e)}")
return []
def _calculate_engagement_score(
self,
scheduled_time: datetime,
content_type: ContentType,
priority: int
) -> float:
"""Calculate engagement score for a given time and content type."""
try:
score = 0.0
# Base score from priority
score += priority * 10
# Hour of day factor
hour = scheduled_time.hour
if content_type in self.content_type_patterns:
optimal_hours = self.content_type_patterns[content_type]['peak_hours']
if hour in optimal_hours:
score += 50
else:
# Penalty for non-optimal hours
min_distance = min(abs(hour - oh) for oh in optimal_hours)
score += max(0, 30 - min_distance * 5)
# Day of week factor
day_of_week = scheduled_time.weekday() # 0 = Monday, 6 = Sunday
if content_type == ContentType.ARTICLE:
# Articles perform better on weekdays
if day_of_week < 5: # Monday to Friday
score += 20
else:
score += 5
elif content_type == ContentType.VIDEO:
# Videos perform better on weekends and evenings
if day_of_week >= 5 or hour >= 18:
score += 25
else:
score += 10
elif content_type == ContentType.SOCIAL_POST:
# Social posts are consistent throughout the week
score += 15
# Time spacing factor (avoid clustering)
existing_schedules = self.session.query(Schedule).filter(
Schedule.scheduled_time.between(
scheduled_time - timedelta(hours=2),
scheduled_time + timedelta(hours=2)
)
).all()
if len(existing_schedules) > 3:
score -= len(existing_schedules) * 5
return max(score, 0.0)
except Exception as e:
self.logger.error(f"Error calculating engagement score: {str(e)}")
return 0.0
def _find_optimal_time(
self,
schedule: Schedule,
content_item: ContentItem
) -> Tuple[datetime, float]:
"""Find the optimal time for a schedule."""
try:
best_time = schedule.scheduled_time
best_score = self._calculate_engagement_score(
schedule.scheduled_time,
content_item.content_type,
schedule.priority
)
# Search within a week of the original time
base_date = schedule.scheduled_time.date()
for day_offset in range(-3, 4): # ±3 days
candidate_date = base_date + timedelta(days=day_offset)
# Get optimal hours for this content type
if content_item.content_type in self.content_type_patterns:
optimal_hours = self.content_type_patterns[content_item.content_type]['peak_hours']
else:
optimal_hours = [9, 12, 15, 18]
for hour in optimal_hours:
candidate_time = datetime.combine(candidate_date, datetime.min.time()).replace(hour=hour)
score = self._calculate_engagement_score(
candidate_time,
content_item.content_type,
schedule.priority
)
if score > best_score:
best_time = candidate_time
best_score = score
return best_time, best_score
except Exception as e:
self.logger.error(f"Error finding optimal time: {str(e)}")
return schedule.scheduled_time, 0.0
def _find_conflict_free_time(
self,
preferred_time: datetime,
existing_times: List[datetime],
schedule: Schedule,
min_gap: timedelta = timedelta(minutes=30)
) -> datetime:
"""Find a conflict-free time close to the preferred time."""
try:
# Check if preferred time has conflicts
has_conflict = any(
abs((preferred_time - existing_time).total_seconds()) < min_gap.total_seconds()
for existing_time in existing_times
)
if not has_conflict:
return preferred_time
# Search for nearby conflict-free times
for offset_minutes in [30, 60, 90, 120, -30, -60, -90, -120]:
candidate_time = preferred_time + timedelta(minutes=offset_minutes)
has_conflict = any(
abs((candidate_time - existing_time).total_seconds()) < min_gap.total_seconds()
for existing_time in existing_times
)
if not has_conflict:
return candidate_time
# If no conflict-free time found nearby, return preferred time
return preferred_time
except Exception as e:
self.logger.error(f"Error finding conflict-free time: {str(e)}")
return preferred_time
def _generate_optimization_reason(
self,
original_time: datetime,
optimized_time: datetime,
content_type: ContentType,
improvement_score: float
) -> str:
"""Generate a human-readable optimization reason."""
try:
if improvement_score <= 0:
return "Current time is already optimal"
reasons = []
# Time difference
time_diff = optimized_time - original_time
if abs(time_diff.total_seconds()) > 3600: # More than 1 hour
if time_diff.total_seconds() > 0:
reasons.append(f"Moved {time_diff.total_seconds() / 3600:.1f} hours later")
else:
reasons.append(f"Moved {abs(time_diff.total_seconds()) / 3600:.1f} hours earlier")
# Hour optimization
original_hour = original_time.hour
optimized_hour = optimized_time.hour
if content_type in self.content_type_patterns:
optimal_hours = self.content_type_patterns[content_type]['peak_hours']
if optimized_hour in optimal_hours and original_hour not in optimal_hours:
reasons.append(f"Moved to peak engagement hour ({optimized_hour}:00)")
# Day optimization
original_day = original_time.strftime('%A')
optimized_day = optimized_time.strftime('%A')
if original_day != optimized_day:
reasons.append(f"Moved from {original_day} to {optimized_day}")
# Improvement score
reasons.append(f"Expected {improvement_score:.1f}% engagement improvement")
return "; ".join(reasons) if reasons else "Optimized for better engagement"
except Exception as e:
self.logger.error(f"Error generating optimization reason: {str(e)}")
return "Optimized for better engagement"
def _get_time_suggestion_reason(self, time: datetime, content_type: ContentType) -> str:
"""Get reason for suggesting a specific time."""
try:
reasons = []
hour = time.hour
day_name = time.strftime('%A')
# Hour-based reasons
if content_type in self.content_type_patterns:
optimal_hours = self.content_type_patterns[content_type]['peak_hours']
if hour in optimal_hours:
reasons.append(f"Peak engagement hour for {content_type.value}")
# Day-based reasons
if content_type == ContentType.ARTICLE and time.weekday() < 5:
reasons.append("Weekday optimal for articles")
elif content_type == ContentType.VIDEO and (time.weekday() >= 5 or hour >= 18):
reasons.append("Evening/weekend optimal for videos")
return "; ".join(reasons) if reasons else f"Good time for {content_type.value}"
except Exception as e:
self.logger.error(f"Error getting suggestion reason: {str(e)}")
return "Recommended time"
def analyze_schedule_performance(self, days_back: int = 30) -> Dict[str, Any]:
"""Analyze historical schedule performance."""
try:
# Get schedules from the last N days
cutoff_date = datetime.now() - timedelta(days=days_back)
schedules = self.session.query(Schedule).filter(
Schedule.created_at >= cutoff_date
).all()
if not schedules:
return {'error': 'No schedules found for analysis'}
# Analyze by hour
hour_performance = defaultdict(list)
day_performance = defaultdict(list)
content_type_performance = defaultdict(list)
for schedule in schedules:
content_item = self.session.query(ContentItem).filter(
ContentItem.id == schedule.content_item_id
).first()
if content_item:
hour = schedule.scheduled_time.hour
day = schedule.scheduled_time.strftime('%A')
# Calculate performance score (simplified)
performance_score = self._calculate_performance_score(schedule)
hour_performance[hour].append(performance_score)
day_performance[day].append(performance_score)
content_type_performance[content_item.content_type.value].append(performance_score)
# Calculate averages
analysis = {
'total_schedules': len(schedules),
'analysis_period_days': days_back,
'best_hours': self._get_top_performers(hour_performance),
'best_days': self._get_top_performers(day_performance),
'content_type_performance': self._get_top_performers(content_type_performance),
'recommendations': self._generate_performance_recommendations(
hour_performance,
day_performance,
content_type_performance
)
}
return analysis
except Exception as e:
self.logger.error(f"Error analyzing schedule performance: {str(e)}")
return {'error': str(e)}
def _calculate_performance_score(self, schedule: Schedule) -> float:
"""Calculate a performance score for a schedule (simplified)."""
try:
# This is a simplified performance calculation
# In a real implementation, this would use actual engagement metrics
base_score = 50.0 # Base performance
# Status-based scoring
if schedule.status == ScheduleStatus.COMPLETED:
base_score += 30
elif schedule.status == ScheduleStatus.RUNNING:
base_score += 15
elif schedule.status == ScheduleStatus.FAILED:
base_score -= 20
# Priority-based scoring
base_score += schedule.priority * 2
return max(base_score, 0.0)
except Exception as e:
self.logger.error(f"Error calculating performance score: {str(e)}")
return 0.0
def _get_top_performers(self, performance_data: Dict[str, List[float]]) -> List[Dict[str, Any]]:
"""Get top performing items from performance data."""
try:
performers = []
for key, scores in performance_data.items():
if scores:
avg_score = np.mean(scores)
performers.append({
'key': key,
'average_score': avg_score,
'sample_count': len(scores)
})
# Sort by average score
performers.sort(key=lambda x: x['average_score'], reverse=True)
return performers[:5] # Top 5
except Exception as e:
self.logger.error(f"Error getting top performers: {str(e)}")
return []
def _generate_performance_recommendations(
self,
hour_performance: Dict[int, List[float]],
day_performance: Dict[str, List[float]],
content_type_performance: Dict[str, List[float]]
) -> List[str]:
"""Generate performance-based recommendations."""
try:
recommendations = []
# Hour recommendations
if hour_performance:
best_hours = self._get_top_performers(hour_performance)
if best_hours:
best_hour = best_hours[0]['key']
recommendations.append(f"Schedule more content around {best_hour}:00 for better performance")
# Day recommendations
if day_performance:
best_days = self._get_top_performers(day_performance)
if best_days:
best_day = best_days[0]['key']
recommendations.append(f"Consider scheduling more content on {best_day}s")
# Content type recommendations
if content_type_performance:
best_types = self._get_top_performers(content_type_performance)
if best_types:
best_type = best_types[0]['key']
recommendations.append(f"{best_type} content shows the best performance")
return recommendations
except Exception as e:
self.logger.error(f"Error generating recommendations: {str(e)}")
return []

View File

@@ -0,0 +1,611 @@
"""
Schedule validation system for content scheduling.
"""
import logging
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional, Tuple
from dataclasses import dataclass
import re
# Use unified database models
from lib.database.models import ContentItem, Schedule, ScheduleStatus, ContentType, Platform, get_session
logger = logging.getLogger(__name__)
@dataclass
class ValidationResult:
"""Result of schedule validation."""
is_valid: bool
errors: List[str]
warnings: List[str]
suggestions: List[str]
confidence: float
class ScheduleValidator:
"""Validate content schedules for compliance and optimization."""
def __init__(self):
"""Initialize the schedule validator."""
self.logger = logger
self.session = get_session()
# Platform-specific validation rules
self.platform_rules = {
Platform.TWITTER: {
'max_text_length': 280,
'max_images': 4,
'max_videos': 1,
'allowed_formats': ['jpg', 'png', 'gif', 'mp4'],
'max_file_size_mb': 5,
'posting_frequency_limit': {'per_hour': 10, 'per_day': 100}
},
Platform.FACEBOOK: {
'max_text_length': 63206,
'max_images': 10,
'max_videos': 1,
'allowed_formats': ['jpg', 'png', 'gif', 'mp4', 'mov'],
'max_file_size_mb': 100,
'posting_frequency_limit': {'per_hour': 5, 'per_day': 25}
},
Platform.LINKEDIN: {
'max_text_length': 3000,
'max_images': 9,
'max_videos': 1,
'allowed_formats': ['jpg', 'png', 'gif', 'mp4'],
'max_file_size_mb': 200,
'posting_frequency_limit': {'per_hour': 3, 'per_day': 20}
},
Platform.INSTAGRAM: {
'max_text_length': 2200,
'max_images': 10,
'max_videos': 1,
'allowed_formats': ['jpg', 'png', 'mp4'],
'max_file_size_mb': 100,
'posting_frequency_limit': {'per_hour': 2, 'per_day': 10}
}
}
# Content type validation rules
self.content_type_rules = {
ContentType.ARTICLE: {
'min_title_length': 10,
'max_title_length': 200,
'min_content_length': 100,
'required_fields': ['title', 'content', 'summary']
},
ContentType.VIDEO: {
'min_duration_sec': 5,
'max_duration_sec': 3600,
'required_fields': ['title', 'description'],
'recommended_formats': ['mp4', 'mov']
},
ContentType.IMAGE: {
'min_width': 400,
'min_height': 400,
'max_width': 4096,
'max_height': 4096,
'required_fields': ['title', 'alt_text']
},
ContentType.SOCIAL_POST: {
'min_length': 10,
'max_length': 500,
'required_fields': ['content']
}
}
def validate_schedule(self, schedule: Schedule) -> ValidationResult:
"""Validate a single schedule.
Args:
schedule: Schedule to validate
Returns:
ValidationResult with validation details
"""
try:
errors = []
warnings = []
suggestions = []
# Get content item details
content_item = self.session.query(ContentItem).filter(
ContentItem.id == schedule.content_item_id
).first()
if not content_item:
return ValidationResult(
is_valid=False,
errors=["Content item not found"],
warnings=[],
suggestions=[],
confidence=0.0
)
# Validate basic schedule properties
basic_validation = self._validate_basic_properties(schedule)
errors.extend(basic_validation['errors'])
warnings.extend(basic_validation['warnings'])
suggestions.extend(basic_validation['suggestions'])
# Validate content properties
content_validation = self._validate_content_properties(content_item)
errors.extend(content_validation['errors'])
warnings.extend(content_validation['warnings'])
suggestions.extend(content_validation['suggestions'])
# Validate timing
timing_validation = self._validate_timing(schedule)
errors.extend(timing_validation['errors'])
warnings.extend(timing_validation['warnings'])
suggestions.extend(timing_validation['suggestions'])
# Validate conflicts
conflict_validation = self._validate_conflicts(schedule)
errors.extend(conflict_validation['errors'])
warnings.extend(conflict_validation['warnings'])
suggestions.extend(conflict_validation['suggestions'])
# Calculate confidence
confidence = self._calculate_validation_confidence(errors, warnings)
return ValidationResult(
is_valid=len(errors) == 0,
errors=errors,
warnings=warnings,
suggestions=suggestions,
confidence=confidence
)
except Exception as e:
self.logger.error(f"Error validating schedule: {str(e)}")
return ValidationResult(
is_valid=False,
errors=[f"Validation error: {str(e)}"],
warnings=[],
suggestions=[],
confidence=0.0
)
def validate_multiple_schedules(self, schedules: List[Schedule]) -> Dict[str, ValidationResult]:
"""Validate multiple schedules and check for cross-schedule issues.
Args:
schedules: List of schedules to validate
Returns:
Dictionary mapping schedule IDs to validation results
"""
try:
results = {}
# Validate individual schedules
for schedule in schedules:
results[str(schedule.id)] = self.validate_schedule(schedule)
# Check for cross-schedule conflicts
cross_validation = self._validate_cross_schedule_conflicts(schedules)
# Add cross-validation issues to individual results
for schedule_id, issues in cross_validation.items():
if schedule_id in results:
results[schedule_id].warnings.extend(issues.get('warnings', []))
results[schedule_id].suggestions.extend(issues.get('suggestions', []))
return results
except Exception as e:
self.logger.error(f"Error validating multiple schedules: {str(e)}")
return {}
def _validate_basic_properties(self, schedule: Schedule) -> Dict[str, List[str]]:
"""Validate basic schedule properties."""
errors = []
warnings = []
suggestions = []
try:
# Check required fields
if not schedule.content_item_id:
errors.append("Content item ID is required")
if not schedule.scheduled_time:
errors.append("Scheduled time is required")
if not schedule.status:
errors.append("Schedule status is required")
# Check priority range
if schedule.priority < 1 or schedule.priority > 10:
warnings.append(f"Priority {schedule.priority} is outside recommended range (1-10)")
# Check if schedule is in the past
if schedule.scheduled_time < datetime.now():
if schedule.status == ScheduleStatus.PENDING:
errors.append("Cannot schedule content in the past")
else:
warnings.append("Schedule time is in the past")
# Check if schedule is too far in the future
max_future_days = 365 # 1 year
if schedule.scheduled_time > datetime.now() + timedelta(days=max_future_days):
warnings.append(f"Schedule is more than {max_future_days} days in the future")
suggestions.append("Consider scheduling closer to the current date for better relevance")
# Validate recurrence pattern
if schedule.recurrence:
recurrence_validation = self._validate_recurrence_pattern(schedule.recurrence)
errors.extend(recurrence_validation['errors'])
warnings.extend(recurrence_validation['warnings'])
suggestions.extend(recurrence_validation['suggestions'])
except Exception as e:
self.logger.error(f"Error validating basic properties: {str(e)}")
errors.append(f"Basic validation error: {str(e)}")
return {'errors': errors, 'warnings': warnings, 'suggestions': suggestions}
def _validate_content_properties(self, content_item: ContentItem) -> Dict[str, List[str]]:
"""Validate content item properties."""
errors = []
warnings = []
suggestions = []
try:
# Check required fields
if not content_item.title or len(content_item.title.strip()) == 0:
errors.append("Content title is required")
if not content_item.content or len(content_item.content.strip()) == 0:
errors.append("Content body is required")
# Validate based on content type
if content_item.content_type:
type_rules = self.content_type_rules.get(content_item.content_type)
if type_rules:
type_validation = self._validate_content_type_rules(content_item, type_rules)
errors.extend(type_validation['errors'])
warnings.extend(type_validation['warnings'])
suggestions.extend(type_validation['suggestions'])
# Check for potentially problematic content
content_check = self._check_content_quality(content_item)
warnings.extend(content_check['warnings'])
suggestions.extend(content_check['suggestions'])
except Exception as e:
self.logger.error(f"Error validating content properties: {str(e)}")
errors.append(f"Content validation error: {str(e)}")
return {'errors': errors, 'warnings': warnings, 'suggestions': suggestions}
def _validate_timing(self, schedule: Schedule) -> Dict[str, List[str]]:
"""Validate schedule timing."""
errors = []
warnings = []
suggestions = []
try:
scheduled_time = schedule.scheduled_time
# Check if it's a reasonable time to post
hour = scheduled_time.hour
day_of_week = scheduled_time.weekday() # 0 = Monday, 6 = Sunday
# Check for very early or very late hours
if hour < 6 or hour > 23:
warnings.append(f"Scheduled for {hour}:00 - consider posting during peak hours (6 AM - 11 PM)")
suggestions.append("Peak engagement typically occurs between 9 AM and 9 PM")
# Check for weekend posting (depending on content type)
content_item = self.session.query(ContentItem).filter(
ContentItem.id == schedule.content_item_id
).first()
if content_item and content_item.content_type == ContentType.ARTICLE:
if day_of_week >= 5: # Weekend
warnings.append("Business content typically performs better on weekdays")
suggestions.append("Consider rescheduling to Monday-Friday for better engagement")
# Check for holidays or special dates (simplified)
if self._is_holiday(scheduled_time.date()):
warnings.append("Scheduled for a holiday - engagement may be lower")
suggestions.append("Consider rescheduling to avoid holidays for better reach")
# Check frequency limits
frequency_check = self._check_posting_frequency(schedule)
warnings.extend(frequency_check['warnings'])
suggestions.extend(frequency_check['suggestions'])
except Exception as e:
self.logger.error(f"Error validating timing: {str(e)}")
errors.append(f"Timing validation error: {str(e)}")
return {'errors': errors, 'warnings': warnings, 'suggestions': suggestions}
def _validate_conflicts(self, schedule: Schedule) -> Dict[str, List[str]]:
"""Validate for scheduling conflicts."""
errors = []
warnings = []
suggestions = []
try:
# Check for nearby schedules
time_window = timedelta(minutes=30)
nearby_schedules = self.session.query(Schedule).filter(
Schedule.id != schedule.id,
Schedule.scheduled_time.between(
schedule.scheduled_time - time_window,
schedule.scheduled_time + time_window
)
).all()
if nearby_schedules:
warnings.append(f"Found {len(nearby_schedules)} other schedule(s) within 30 minutes")
suggestions.append("Consider spacing schedules at least 30 minutes apart for better visibility")
# Check for same-day content overload
same_day_schedules = self.session.query(Schedule).filter(
Schedule.id != schedule.id,
Schedule.scheduled_time >= schedule.scheduled_time.replace(hour=0, minute=0, second=0),
Schedule.scheduled_time < schedule.scheduled_time.replace(hour=0, minute=0, second=0) + timedelta(days=1)
).all()
if len(same_day_schedules) > 5:
warnings.append(f"Found {len(same_day_schedules)} other schedules on the same day")
suggestions.append("Consider distributing content across multiple days to avoid overwhelming your audience")
except Exception as e:
self.logger.error(f"Error validating conflicts: {str(e)}")
errors.append(f"Conflict validation error: {str(e)}")
return {'errors': errors, 'warnings': warnings, 'suggestions': suggestions}
def _validate_recurrence_pattern(self, recurrence: str) -> Dict[str, List[str]]:
"""Validate recurrence pattern."""
errors = []
warnings = []
suggestions = []
try:
# Define valid recurrence patterns
valid_patterns = [
'daily', 'weekly', 'monthly', 'yearly',
'weekdays', 'weekends',
'every 2 days', 'every 3 days', 'every 7 days',
'every 2 weeks', 'every 2 months'
]
if recurrence.lower() not in valid_patterns:
# Check if it's a cron-like pattern
if not self._is_valid_cron_pattern(recurrence):
errors.append(f"Invalid recurrence pattern: {recurrence}")
suggestions.append(f"Valid patterns include: {', '.join(valid_patterns[:5])}")
# Check for overly frequent recurrence
if 'hour' in recurrence.lower():
warnings.append("Hourly recurrence may overwhelm your audience")
suggestions.append("Consider daily or weekly recurrence for better engagement")
except Exception as e:
self.logger.error(f"Error validating recurrence: {str(e)}")
errors.append(f"Recurrence validation error: {str(e)}")
return {'errors': errors, 'warnings': warnings, 'suggestions': suggestions}
def _validate_content_type_rules(self, content_item: ContentItem, rules: Dict[str, Any]) -> Dict[str, List[str]]:
"""Validate content against type-specific rules."""
errors = []
warnings = []
suggestions = []
try:
# Check title length
if 'min_title_length' in rules and len(content_item.title) < rules['min_title_length']:
errors.append(f"Title too short (minimum {rules['min_title_length']} characters)")
if 'max_title_length' in rules and len(content_item.title) > rules['max_title_length']:
errors.append(f"Title too long (maximum {rules['max_title_length']} characters)")
# Check content length
if 'min_content_length' in rules and len(content_item.content) < rules['min_content_length']:
errors.append(f"Content too short (minimum {rules['min_content_length']} characters)")
if 'max_length' in rules and len(content_item.content) > rules['max_length']:
errors.append(f"Content too long (maximum {rules['max_length']} characters)")
# Check required fields
if 'required_fields' in rules:
for field in rules['required_fields']:
if not hasattr(content_item, field) or not getattr(content_item, field):
errors.append(f"Required field missing: {field}")
except Exception as e:
self.logger.error(f"Error validating content type rules: {str(e)}")
errors.append(f"Content type validation error: {str(e)}")
return {'errors': errors, 'warnings': warnings, 'suggestions': suggestions}
def _check_content_quality(self, content_item: ContentItem) -> Dict[str, List[str]]:
"""Check content quality and provide suggestions."""
warnings = []
suggestions = []
try:
content = content_item.content
title = content_item.title
# Check for excessive capitalization
if title and title.isupper():
warnings.append("Title is in all caps")
suggestions.append("Consider using proper capitalization for better readability")
# Check for excessive punctuation
if content and content.count('!') > 3:
warnings.append("Excessive exclamation marks detected")
suggestions.append("Reduce exclamation marks for more professional tone")
# Check for spelling/grammar (simplified)
if content:
# Simple checks for common issues
if ' ' in content: # Double spaces
suggestions.append("Remove extra spaces for cleaner formatting")
if content.count('?') > 5:
warnings.append("Many question marks detected")
suggestions.append("Consider reducing questions for clearer messaging")
# Check for hashtag usage
hashtag_count = len(re.findall(r'#\w+', content)) if content else 0
if hashtag_count > 10:
warnings.append(f"High number of hashtags ({hashtag_count})")
suggestions.append("Consider using 3-5 relevant hashtags for optimal reach")
# Check for URL presence
url_count = len(re.findall(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', content)) if content else 0
if url_count > 2:
warnings.append(f"Multiple URLs detected ({url_count})")
suggestions.append("Consider limiting to 1-2 URLs to avoid appearing spammy")
except Exception as e:
self.logger.error(f"Error checking content quality: {str(e)}")
return {'warnings': warnings, 'suggestions': suggestions}
def _check_posting_frequency(self, schedule: Schedule) -> Dict[str, List[str]]:
"""Check posting frequency limits."""
warnings = []
suggestions = []
try:
# Check hourly frequency
hour_start = schedule.scheduled_time.replace(minute=0, second=0, microsecond=0)
hour_end = hour_start + timedelta(hours=1)
hourly_schedules = self.session.query(Schedule).filter(
Schedule.scheduled_time >= hour_start,
Schedule.scheduled_time < hour_end
).count()
if hourly_schedules > 3:
warnings.append(f"High posting frequency: {hourly_schedules} posts in the same hour")
suggestions.append("Consider spacing posts throughout the day for better engagement")
# Check daily frequency
day_start = schedule.scheduled_time.replace(hour=0, minute=0, second=0, microsecond=0)
day_end = day_start + timedelta(days=1)
daily_schedules = self.session.query(Schedule).filter(
Schedule.scheduled_time >= day_start,
Schedule.scheduled_time < day_end
).count()
if daily_schedules > 10:
warnings.append(f"High daily posting frequency: {daily_schedules} posts")
suggestions.append("Consider reducing daily posts to 3-5 for optimal audience engagement")
except Exception as e:
self.logger.error(f"Error checking posting frequency: {str(e)}")
return {'warnings': warnings, 'suggestions': suggestions}
def _validate_cross_schedule_conflicts(self, schedules: List[Schedule]) -> Dict[str, Dict[str, List[str]]]:
"""Validate conflicts across multiple schedules."""
conflicts = {}
try:
# Sort schedules by time
sorted_schedules = sorted(schedules, key=lambda x: x.scheduled_time)
for i, schedule in enumerate(sorted_schedules):
schedule_id = str(schedule.id)
conflicts[schedule_id] = {'warnings': [], 'suggestions': []}
# Check with subsequent schedules
for j in range(i + 1, len(sorted_schedules)):
other_schedule = sorted_schedules[j]
time_diff = other_schedule.scheduled_time - schedule.scheduled_time
# Check if schedules are too close
if time_diff < timedelta(minutes=15):
conflicts[schedule_id]['warnings'].append(
f"Schedule conflicts with another schedule {time_diff.total_seconds() / 60:.0f} minutes later"
)
conflicts[schedule_id]['suggestions'].append(
"Consider spacing schedules at least 15 minutes apart"
)
# Stop checking if schedules are more than 2 hours apart
if time_diff > timedelta(hours=2):
break
except Exception as e:
self.logger.error(f"Error validating cross-schedule conflicts: {str(e)}")
return conflicts
def _calculate_validation_confidence(self, errors: List[str], warnings: List[str]) -> float:
"""Calculate confidence in validation results."""
try:
# Start with full confidence
confidence = 1.0
# Reduce confidence based on errors and warnings
confidence -= len(errors) * 0.2 # Each error reduces confidence by 20%
confidence -= len(warnings) * 0.05 # Each warning reduces confidence by 5%
# Ensure confidence is between 0 and 1
return max(0.0, min(1.0, confidence))
except Exception as e:
self.logger.error(f"Error calculating validation confidence: {str(e)}")
return 0.0
def _is_holiday(self, date) -> bool:
"""Check if a date is a holiday (simplified implementation)."""
try:
# This is a simplified implementation
# In a real system, you would use a proper holiday library
# Check for some common holidays
month = date.month
day = date.day
# New Year's Day
if month == 1 and day == 1:
return True
# Christmas
if month == 12 and day == 25:
return True
# Independence Day (US)
if month == 7 and day == 4:
return True
return False
except Exception as e:
self.logger.error(f"Error checking holiday: {str(e)}")
return False
def _is_valid_cron_pattern(self, pattern: str) -> bool:
"""Check if a string is a valid cron pattern (simplified)."""
try:
# This is a very simplified cron validation
# A proper implementation would use a cron parsing library
parts = pattern.split()
if len(parts) != 5:
return False
# Basic validation for each part
for part in parts:
if not (part.isdigit() or part == '*' or '/' in part or '-' in part or ',' in part):
return False
return True
except Exception as e:
self.logger.error(f"Error validating cron pattern: {str(e)}")
return False

View File

@@ -0,0 +1,402 @@
"""
Core scheduler implementation using APScheduler.
"""
import logging
import asyncio
from typing import Dict, Any, List, Optional, Union
from datetime import datetime, timedelta
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor
from apscheduler.triggers.date import DateTrigger
from apscheduler.triggers.cron import CronTrigger
from apscheduler.events import EVENT_JOB_ERROR, EVENT_JOB_EXECUTED, EVENT_JOB_MISSED
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
# Use unified database models
from lib.database.models import ContentItem, Schedule, ScheduleStatus, get_engine, get_session, init_db
from ..utils.error_handling import SchedulingError
from .conflict_resolver import ConflictResolver
from .health_checker import ScheduleHealthChecker
from .schedule_validator import ScheduleValidator
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
class ContentScheduler:
"""Core content scheduler implementation."""
def __init__(
self,
db_url: str = "sqlite:///content_scheduler.db",
max_workers: int = 10,
job_timeout: int = 300,
max_retries: int = 3,
retry_delay: int = 300,
health_check_interval: int = 300,
validation_config: Dict[str, Any] = None
):
"""Initialize the content scheduler.
Args:
db_url: Database URL for job persistence
max_workers: Maximum number of worker threads
job_timeout: Job execution timeout in seconds
max_retries: Maximum number of retry attempts
retry_delay: Delay between retries in seconds
health_check_interval: Health check interval in seconds
validation_config: Configuration for schedule validation
"""
self.logger = logger
self.db_url = db_url
self.max_workers = max_workers
self.job_timeout = job_timeout
self.max_retries = max_retries
self.retry_delay = retry_delay
# Use unified database connection
self.engine = get_engine(db_url)
init_db(self.engine)
self.Session = sessionmaker(bind=self.engine)
# Initialize job stores
self.jobstores = {
'default': SQLAlchemyJobStore(url=db_url)
}
# Initialize executors
self.executors = {
'default': ThreadPoolExecutor(max_workers),
'processpool': ProcessPoolExecutor(max_workers)
}
# Initialize scheduler
self.scheduler = AsyncIOScheduler(
jobstores=self.jobstores,
executors=self.executors,
timezone='UTC',
job_defaults={
'coalesce': True,
'max_instances': 1,
'misfire_grace_time': 60
}
)
# Initialize conflict resolver
self.conflict_resolver = ConflictResolver()
# Initialize health checker
self.health_checker = ScheduleHealthChecker(
scheduler=self,
check_interval=health_check_interval
)
# Initialize validator
self.validator = ScheduleValidator(validation_config or {})
# Add event listeners
self.scheduler.add_listener(
self._handle_job_event,
EVENT_JOB_EXECUTED | EVENT_JOB_ERROR | EVENT_JOB_MISSED
)
# Track active jobs
self.active_jobs = {}
self.job_stats = {
'total_scheduled': 0,
'successful': 0,
'failed': 0,
'retries': 0
}
async def start(self):
"""Start the scheduler."""
try:
if not self.scheduler.running:
self.scheduler.start()
await self._recover_jobs()
await self.health_checker.start()
self.logger.info("Content scheduler started successfully")
except Exception as e:
self.logger.error(f"Failed to start scheduler: {str(e)}")
raise SchedulingError(f"Scheduler startup failed: {str(e)}")
async def stop(self):
"""Stop the scheduler."""
try:
if self.scheduler.running:
self.scheduler.shutdown(wait=True)
await self.health_checker.stop()
self.logger.info("Content scheduler stopped successfully")
except Exception as e:
self.logger.error(f"Failed to stop scheduler: {str(e)}")
raise SchedulingError(f"Scheduler shutdown failed: {str(e)}")
async def schedule_content(self, content_item: ContentItem, schedule_time: datetime,
platforms: List[str], recurrence: str = None,
validate: bool = True) -> str:
"""Schedule content for publishing.
Args:
content_item: ContentItem to schedule
schedule_time: When to publish
platforms: List of platforms to publish to
recurrence: Recurrence pattern (optional)
validate: Whether to validate the schedule
Returns:
Schedule ID
"""
try:
session = self.Session()
# Create schedule record
schedule = Schedule(
content_item_id=content_item.id,
scheduled_time=schedule_time,
status=ScheduleStatus.SCHEDULED,
recurrence=recurrence,
priority=1
)
session.add(schedule)
session.commit()
# Schedule the job
if recurrence:
job_id = await self._schedule_recurring(schedule, platforms)
else:
job_id = await self._schedule_one_time(schedule, platforms)
# Update schedule with job ID
schedule.result = f"job_id:{job_id}"
session.commit()
session.close()
self.job_stats['total_scheduled'] += 1
self.logger.info(f"Scheduled content {content_item.id} for {schedule_time}")
return str(schedule.id)
except Exception as e:
self.logger.error(f"Failed to schedule content: {str(e)}")
if 'session' in locals():
session.rollback()
session.close()
raise SchedulingError(f"Content scheduling failed: {str(e)}")
async def _schedule_one_time(self, schedule: Schedule, platforms: List[str]) -> str:
"""Schedule a one-time content publish.
Args:
schedule: Schedule object
platforms: List of platforms
Returns:
Job ID
"""
try:
job_id = f"one_time_{schedule.content_item_id}_{int(schedule.scheduled_time.timestamp())}"
self.scheduler.add_job(
self._run_async_job,
trigger=DateTrigger(run_date=schedule.scheduled_time),
args=[schedule, platforms],
id=job_id,
replace_existing=True,
misfire_grace_time=self.job_timeout
)
return job_id
except Exception as e:
self.logger.error(f"Failed to schedule one-time job: {str(e)}")
raise SchedulingError(f"One-time scheduling failed: {str(e)}")
async def _schedule_recurring(self, schedule: Schedule, platforms: List[str]) -> str:
"""Schedule a recurring content publish.
Args:
schedule: Schedule object
platforms: List of platforms
Returns:
Job ID
"""
try:
job_id = f"recurring_{schedule.content_item_id}_{int(datetime.utcnow().timestamp())}"
# Parse recurrence pattern (simplified)
if schedule.recurrence == "daily":
trigger = CronTrigger(hour=schedule.scheduled_time.hour, minute=schedule.scheduled_time.minute)
elif schedule.recurrence == "weekly":
trigger = CronTrigger(day_of_week=schedule.scheduled_time.weekday(),
hour=schedule.scheduled_time.hour,
minute=schedule.scheduled_time.minute)
else:
# Default to daily
trigger = CronTrigger(hour=schedule.scheduled_time.hour, minute=schedule.scheduled_time.minute)
self.scheduler.add_job(
self._run_async_job,
trigger=trigger,
args=[schedule, platforms],
id=job_id,
replace_existing=True,
misfire_grace_time=self.job_timeout
)
return job_id
except Exception as e:
self.logger.error(f"Failed to schedule recurring job: {str(e)}")
raise SchedulingError(f"Recurring scheduling failed: {str(e)}")
async def _run_async_job(self, schedule: Schedule, platforms: List[str]):
"""Run an async job in the event loop.
Args:
schedule: Schedule object
platforms: List of platforms
"""
try:
await self._publish_content(schedule, platforms)
except Exception as e:
self.logger.error(f"Job execution failed: {str(e)}")
await self._handle_job_failure(schedule, str(e))
async def _publish_content(self, schedule: Schedule, platforms: List[str]):
"""Publish content to specified platforms.
Args:
schedule: Schedule object
platforms: List of platforms
"""
try:
session = self.Session()
content_item = session.query(ContentItem).get(schedule.content_item_id)
if not content_item:
raise SchedulingError(f"Content item {schedule.content_item_id} not found")
# Update schedule status
schedule.status = ScheduleStatus.RUNNING
session.commit()
# Simulate content publishing (replace with actual platform publishing logic)
self.logger.info(f"Publishing content '{content_item.title}' to platforms: {platforms}")
# Mark as completed
schedule.status = ScheduleStatus.COMPLETED
schedule.result = f"Published to {', '.join(platforms)} at {datetime.utcnow()}"
session.commit()
session.close()
self.job_stats['successful'] += 1
except Exception as e:
session = self.Session()
schedule.status = ScheduleStatus.FAILED
schedule.result = f"Failed: {str(e)}"
session.commit()
session.close()
self.job_stats['failed'] += 1
raise
async def _handle_job_failure(self, schedule: Schedule, error: str):
"""Handle job failure and retry logic.
Args:
schedule: Schedule object
error: Error message
"""
try:
session = self.Session()
schedule.status = ScheduleStatus.FAILED
schedule.result = f"Failed: {error}"
session.commit()
session.close()
self.job_stats['failed'] += 1
self.logger.error(f"Job failed for schedule {schedule.id}: {error}")
except Exception as e:
self.logger.error(f"Error handling job failure: {str(e)}")
def _handle_job_event(self, event):
"""Handle scheduler events.
Args:
event: Scheduler event
"""
try:
job_id = event.job_id
if event.code == EVENT_JOB_EXECUTED:
self.logger.info(f"Job {job_id} executed successfully")
elif event.code == EVENT_JOB_ERROR:
self.logger.error(f"Job {job_id} failed: {str(event.exception)}")
elif event.code == EVENT_JOB_MISSED:
self.logger.warning(f"Job {job_id} missed execution time")
except Exception as e:
self.logger.error(f"Error handling job event: {str(e)}")
async def _recover_jobs(self):
"""Recover pending jobs from the database."""
try:
session = self.Session()
# Get all scheduled jobs
pending_schedules = session.query(Schedule).filter(
Schedule.status == ScheduleStatus.SCHEDULED
).all()
# Reschedule each job
for schedule in pending_schedules:
try:
content_item = session.query(ContentItem).get(schedule.content_item_id)
if content_item:
platforms = content_item.platforms if isinstance(content_item.platforms, list) else []
await self.schedule_content(content_item, schedule.scheduled_time, platforms,
schedule.recurrence, validate=False)
except Exception as e:
self.logger.error(f"Failed to recover schedule {schedule.id}: {str(e)}")
session.close()
except Exception as e:
self.logger.error(f"Job recovery failed: {str(e)}")
raise SchedulingError(f"Job recovery failed: {str(e)}")
def get_job_stats(self) -> Dict[str, int]:
"""Get job statistics.
Returns:
Dictionary with job statistics
"""
return self.job_stats.copy()
def get_active_jobs(self) -> List[Dict[str, Any]]:
"""Get list of active jobs.
Returns:
List of active job information
"""
try:
jobs = []
for job in self.scheduler.get_jobs():
jobs.append({
'id': job.id,
'next_run_time': job.next_run_time.isoformat() if job.next_run_time else None,
'trigger': str(job.trigger)
})
return jobs
except Exception as e:
self.logger.error(f"Error getting active jobs: {str(e)}")
return []

View File

@@ -0,0 +1,651 @@
"""
Calendar integration for content scheduling.
"""
import logging
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional, Tuple
from dataclasses import dataclass
import json
# Use unified database models
from lib.database.models import ContentItem, Schedule, ScheduleStatus, ContentType, Platform, get_session
logger = logging.getLogger(__name__)
@dataclass
class CalendarEvent:
"""Calendar event representation."""
id: str
title: str
description: str
start_time: datetime
end_time: datetime
location: Optional[str] = None
attendees: List[str] = None
event_type: str = "content_schedule"
metadata: Dict[str, Any] = None
class CalendarIntegration:
"""Integration with calendar systems for content scheduling."""
def __init__(self, calendar_provider: str = "google"):
"""Initialize calendar integration.
Args:
calendar_provider: Calendar provider (google, outlook, etc.)
"""
self.logger = logger
self.session = get_session()
self.calendar_provider = calendar_provider
# Calendar provider configurations
self.provider_configs = {
'google': {
'api_endpoint': 'https://www.googleapis.com/calendar/v3',
'scopes': ['https://www.googleapis.com/auth/calendar'],
'event_duration_minutes': 30
},
'outlook': {
'api_endpoint': 'https://graph.microsoft.com/v1.0',
'scopes': ['https://graph.microsoft.com/calendars.readwrite'],
'event_duration_minutes': 30
},
'apple': {
'api_endpoint': 'https://caldav.icloud.com',
'scopes': ['calendar'],
'event_duration_minutes': 30
}
}
# Event templates for different content types
self.event_templates = {
ContentType.ARTICLE: {
'title_prefix': '📝 Publish Article:',
'description_template': 'Publish article "{title}" to {platforms}',
'duration_minutes': 15
},
ContentType.VIDEO: {
'title_prefix': '🎥 Publish Video:',
'description_template': 'Publish video "{title}" to {platforms}',
'duration_minutes': 30
},
ContentType.IMAGE: {
'title_prefix': '📸 Publish Image:',
'description_template': 'Publish image "{title}" to {platforms}',
'duration_minutes': 10
},
ContentType.SOCIAL_POST: {
'title_prefix': '📱 Social Post:',
'description_template': 'Publish social post "{title}" to {platforms}',
'duration_minutes': 5
}
}
def sync_schedules_to_calendar(self, schedules: List[Schedule] = None) -> Dict[str, Any]:
"""Sync content schedules to calendar.
Args:
schedules: List of schedules to sync (if None, sync all pending schedules)
Returns:
Dictionary with sync results
"""
try:
if schedules is None:
schedules = self.session.query(Schedule).filter(
Schedule.status == ScheduleStatus.PENDING
).all()
sync_results = {
'total_schedules': len(schedules),
'synced_successfully': 0,
'failed_syncs': 0,
'errors': [],
'created_events': []
}
for schedule in schedules:
try:
# Get content item details
content_item = self.session.query(ContentItem).filter(
ContentItem.id == schedule.content_item_id
).first()
if not content_item:
sync_results['errors'].append(f"Content item not found for schedule {schedule.id}")
sync_results['failed_syncs'] += 1
continue
# Create calendar event
event = self._create_calendar_event(schedule, content_item)
# Sync to calendar provider
event_id = self._sync_event_to_provider(event)
if event_id:
# Update schedule with calendar event ID
schedule.metadata = schedule.metadata or {}
schedule.metadata['calendar_event_id'] = event_id
self.session.commit()
sync_results['synced_successfully'] += 1
sync_results['created_events'].append({
'schedule_id': schedule.id,
'event_id': event_id,
'title': event.title
})
else:
sync_results['failed_syncs'] += 1
sync_results['errors'].append(f"Failed to create calendar event for schedule {schedule.id}")
except Exception as e:
self.logger.error(f"Error syncing schedule {schedule.id}: {str(e)}")
sync_results['failed_syncs'] += 1
sync_results['errors'].append(f"Schedule {schedule.id}: {str(e)}")
return sync_results
except Exception as e:
self.logger.error(f"Error syncing schedules to calendar: {str(e)}")
return {
'total_schedules': 0,
'synced_successfully': 0,
'failed_syncs': 0,
'errors': [f"Sync error: {str(e)}"],
'created_events': []
}
def import_calendar_events(self, calendar_id: str = None, date_range: Tuple[datetime, datetime] = None) -> Dict[str, Any]:
"""Import events from calendar and suggest content schedules.
Args:
calendar_id: Calendar ID to import from
date_range: Date range to import events from
Returns:
Dictionary with import results and suggestions
"""
try:
if date_range is None:
start_date = datetime.now()
end_date = start_date + timedelta(days=30)
date_range = (start_date, end_date)
# Get events from calendar provider
events = self._get_events_from_provider(calendar_id, date_range)
import_results = {
'total_events': len(events),
'content_suggestions': [],
'scheduling_gaps': [],
'optimal_times': []
}
# Analyze events for content scheduling opportunities
for event in events:
suggestions = self._analyze_event_for_content_opportunities(event)
import_results['content_suggestions'].extend(suggestions)
# Find scheduling gaps
gaps = self._find_scheduling_gaps(events, date_range)
import_results['scheduling_gaps'] = gaps
# Suggest optimal posting times
optimal_times = self._suggest_optimal_posting_times(events, date_range)
import_results['optimal_times'] = optimal_times
return import_results
except Exception as e:
self.logger.error(f"Error importing calendar events: {str(e)}")
return {
'total_events': 0,
'content_suggestions': [],
'scheduling_gaps': [],
'optimal_times': [],
'error': str(e)
}
def create_content_schedule_from_event(self, event: CalendarEvent, content_item_id: int) -> Optional[Schedule]:
"""Create a content schedule from a calendar event.
Args:
event: Calendar event
content_item_id: ID of content item to schedule
Returns:
Created schedule or None if failed
"""
try:
# Get content item
content_item = self.session.query(ContentItem).filter(
ContentItem.id == content_item_id
).first()
if not content_item:
self.logger.error(f"Content item {content_item_id} not found")
return None
# Create schedule
schedule = Schedule(
content_item_id=content_item_id,
scheduled_time=event.start_time,
status=ScheduleStatus.PENDING,
priority=5, # Default priority
metadata={
'calendar_event_id': event.id,
'created_from_calendar': True,
'original_event_title': event.title
}
)
self.session.add(schedule)
self.session.commit()
self.logger.info(f"Created schedule {schedule.id} from calendar event {event.id}")
return schedule
except Exception as e:
self.logger.error(f"Error creating schedule from event: {str(e)}")
self.session.rollback()
return None
def update_calendar_event_from_schedule(self, schedule: Schedule) -> bool:
"""Update calendar event when schedule changes.
Args:
schedule: Updated schedule
Returns:
True if successful, False otherwise
"""
try:
# Check if schedule has associated calendar event
if not schedule.metadata or 'calendar_event_id' not in schedule.metadata:
return False
event_id = schedule.metadata['calendar_event_id']
# Get content item
content_item = self.session.query(ContentItem).filter(
ContentItem.id == schedule.content_item_id
).first()
if not content_item:
return False
# Create updated event
updated_event = self._create_calendar_event(schedule, content_item)
updated_event.id = event_id
# Update event in calendar provider
success = self._update_event_in_provider(updated_event)
if success:
self.logger.info(f"Updated calendar event {event_id} for schedule {schedule.id}")
else:
self.logger.error(f"Failed to update calendar event {event_id}")
return success
except Exception as e:
self.logger.error(f"Error updating calendar event: {str(e)}")
return False
def delete_calendar_event_from_schedule(self, schedule: Schedule) -> bool:
"""Delete calendar event when schedule is deleted.
Args:
schedule: Schedule being deleted
Returns:
True if successful, False otherwise
"""
try:
# Check if schedule has associated calendar event
if not schedule.metadata or 'calendar_event_id' not in schedule.metadata:
return True # No event to delete
event_id = schedule.metadata['calendar_event_id']
# Delete event from calendar provider
success = self._delete_event_from_provider(event_id)
if success:
self.logger.info(f"Deleted calendar event {event_id} for schedule {schedule.id}")
else:
self.logger.error(f"Failed to delete calendar event {event_id}")
return success
except Exception as e:
self.logger.error(f"Error deleting calendar event: {str(e)}")
return False
def get_calendar_view(self, date_range: Tuple[datetime, datetime] = None) -> Dict[str, Any]:
"""Get calendar view of scheduled content.
Args:
date_range: Date range for calendar view
Returns:
Dictionary with calendar view data
"""
try:
if date_range is None:
start_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
end_date = start_date + timedelta(days=30)
date_range = (start_date, end_date)
# Get schedules in date range
schedules = self.session.query(Schedule).filter(
Schedule.scheduled_time >= date_range[0],
Schedule.scheduled_time <= date_range[1]
).all()
calendar_events = []
for schedule in schedules:
content_item = self.session.query(ContentItem).filter(
ContentItem.id == schedule.content_item_id
).first()
if content_item:
event = self._create_calendar_event(schedule, content_item)
calendar_events.append({
'id': str(schedule.id),
'title': event.title,
'description': event.description,
'start': event.start_time.isoformat(),
'end': event.end_time.isoformat(),
'status': schedule.status.value,
'priority': schedule.priority,
'content_type': content_item.content_type.value if content_item.content_type else 'unknown',
'platforms': schedule.platforms or []
})
# Group events by day
events_by_day = {}
for event in calendar_events:
day = datetime.fromisoformat(event['start']).date()
if day not in events_by_day:
events_by_day[day] = []
events_by_day[day].append(event)
return {
'date_range': {
'start': date_range[0].isoformat(),
'end': date_range[1].isoformat()
},
'total_events': len(calendar_events),
'events': calendar_events,
'events_by_day': {day.isoformat(): events for day, events in events_by_day.items()},
'summary': self._generate_calendar_summary(calendar_events)
}
except Exception as e:
self.logger.error(f"Error getting calendar view: {str(e)}")
return {
'date_range': None,
'total_events': 0,
'events': [],
'events_by_day': {},
'summary': {},
'error': str(e)
}
def _create_calendar_event(self, schedule: Schedule, content_item: ContentItem) -> CalendarEvent:
"""Create calendar event from schedule and content item."""
try:
# Get event template based on content type
template = self.event_templates.get(
content_item.content_type,
self.event_templates[ContentType.SOCIAL_POST]
)
# Create event title
title = f"{template['title_prefix']} {content_item.title}"
# Create event description
platforms_str = ', '.join(schedule.platforms) if schedule.platforms else 'Default platforms'
description = template['description_template'].format(
title=content_item.title,
platforms=platforms_str
)
# Add content summary if available
if content_item.summary:
description += f"\n\nSummary: {content_item.summary}"
# Calculate end time
duration = timedelta(minutes=template['duration_minutes'])
end_time = schedule.scheduled_time + duration
# Create metadata
metadata = {
'schedule_id': schedule.id,
'content_item_id': content_item.id,
'content_type': content_item.content_type.value if content_item.content_type else 'unknown',
'platforms': schedule.platforms or [],
'priority': schedule.priority,
'status': schedule.status.value
}
return CalendarEvent(
id=f"schedule_{schedule.id}",
title=title,
description=description,
start_time=schedule.scheduled_time,
end_time=end_time,
metadata=metadata
)
except Exception as e:
self.logger.error(f"Error creating calendar event: {str(e)}")
# Return a basic event as fallback
return CalendarEvent(
id=f"schedule_{schedule.id}",
title=f"Content Schedule: {content_item.title}",
description="Content publishing schedule",
start_time=schedule.scheduled_time,
end_time=schedule.scheduled_time + timedelta(minutes=30)
)
def _sync_event_to_provider(self, event: CalendarEvent) -> Optional[str]:
"""Sync event to calendar provider (mock implementation)."""
try:
# This is a mock implementation
# In a real system, you would integrate with actual calendar APIs
self.logger.info(f"Syncing event to {self.calendar_provider}: {event.title}")
# Simulate API call
event_id = f"{self.calendar_provider}_{event.id}_{int(datetime.now().timestamp())}"
return event_id
except Exception as e:
self.logger.error(f"Error syncing event to provider: {str(e)}")
return None
def _get_events_from_provider(self, calendar_id: str, date_range: Tuple[datetime, datetime]) -> List[CalendarEvent]:
"""Get events from calendar provider (mock implementation)."""
try:
# This is a mock implementation
# In a real system, you would fetch from actual calendar APIs
self.logger.info(f"Fetching events from {self.calendar_provider} calendar {calendar_id}")
# Return empty list for mock
return []
except Exception as e:
self.logger.error(f"Error fetching events from provider: {str(e)}")
return []
def _update_event_in_provider(self, event: CalendarEvent) -> bool:
"""Update event in calendar provider (mock implementation)."""
try:
# This is a mock implementation
self.logger.info(f"Updating event in {self.calendar_provider}: {event.id}")
return True
except Exception as e:
self.logger.error(f"Error updating event in provider: {str(e)}")
return False
def _delete_event_from_provider(self, event_id: str) -> bool:
"""Delete event from calendar provider (mock implementation)."""
try:
# This is a mock implementation
self.logger.info(f"Deleting event from {self.calendar_provider}: {event_id}")
return True
except Exception as e:
self.logger.error(f"Error deleting event from provider: {str(e)}")
return False
def _analyze_event_for_content_opportunities(self, event: CalendarEvent) -> List[Dict[str, Any]]:
"""Analyze calendar event for content opportunities."""
suggestions = []
try:
# Look for keywords that suggest content opportunities
content_keywords = ['meeting', 'conference', 'launch', 'announcement', 'webinar', 'presentation']
event_text = f"{event.title} {event.description}".lower()
for keyword in content_keywords:
if keyword in event_text:
suggestions.append({
'type': 'content_opportunity',
'keyword': keyword,
'suggested_time': event.end_time, # Suggest posting after the event
'content_type': self._suggest_content_type_for_keyword(keyword),
'description': f"Consider creating content about the {keyword}"
})
except Exception as e:
self.logger.error(f"Error analyzing event for opportunities: {str(e)}")
return suggestions
def _find_scheduling_gaps(self, events: List[CalendarEvent], date_range: Tuple[datetime, datetime]) -> List[Dict[str, Any]]:
"""Find gaps in schedule that could be used for content posting."""
gaps = []
try:
# Sort events by start time
sorted_events = sorted(events, key=lambda x: x.start_time)
current_time = date_range[0]
for event in sorted_events:
# Check if there's a gap before this event
if event.start_time > current_time + timedelta(hours=2):
gaps.append({
'start': current_time.isoformat(),
'end': event.start_time.isoformat(),
'duration_hours': (event.start_time - current_time).total_seconds() / 3600,
'suggested_use': 'Content posting opportunity'
})
current_time = max(current_time, event.end_time)
# Check for gap after last event
if current_time < date_range[1] - timedelta(hours=2):
gaps.append({
'start': current_time.isoformat(),
'end': date_range[1].isoformat(),
'duration_hours': (date_range[1] - current_time).total_seconds() / 3600,
'suggested_use': 'Content posting opportunity'
})
except Exception as e:
self.logger.error(f"Error finding scheduling gaps: {str(e)}")
return gaps
def _suggest_optimal_posting_times(self, events: List[CalendarEvent], date_range: Tuple[datetime, datetime]) -> List[Dict[str, Any]]:
"""Suggest optimal times for content posting based on calendar."""
optimal_times = []
try:
# Define optimal posting hours (9 AM, 1 PM, 5 PM)
optimal_hours = [9, 13, 17]
current_date = date_range[0].date()
end_date = date_range[1].date()
while current_date <= end_date:
for hour in optimal_hours:
suggested_time = datetime.combine(current_date, datetime.min.time().replace(hour=hour))
# Check if this time conflicts with any events
conflicts = any(
event.start_time <= suggested_time <= event.end_time
for event in events
)
if not conflicts:
optimal_times.append({
'time': suggested_time.isoformat(),
'reason': f'Optimal posting time ({hour}:00) with no calendar conflicts',
'confidence': 0.8
})
current_date += timedelta(days=1)
except Exception as e:
self.logger.error(f"Error suggesting optimal posting times: {str(e)}")
return optimal_times
def _suggest_content_type_for_keyword(self, keyword: str) -> str:
"""Suggest content type based on keyword."""
keyword_mapping = {
'meeting': 'social_post',
'conference': 'article',
'launch': 'video',
'announcement': 'social_post',
'webinar': 'video',
'presentation': 'article'
}
return keyword_mapping.get(keyword, 'social_post')
def _generate_calendar_summary(self, events: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Generate summary statistics for calendar events."""
try:
if not events:
return {}
# Count by status
status_counts = {}
for event in events:
status = event.get('status', 'unknown')
status_counts[status] = status_counts.get(status, 0) + 1
# Count by content type
type_counts = {}
for event in events:
content_type = event.get('content_type', 'unknown')
type_counts[content_type] = type_counts.get(content_type, 0) + 1
# Count by day
daily_counts = {}
for event in events:
day = datetime.fromisoformat(event['start']).date().isoformat()
daily_counts[day] = daily_counts.get(day, 0) + 1
return {
'total_events': len(events),
'by_status': status_counts,
'by_content_type': type_counts,
'by_day': daily_counts,
'busiest_day': max(daily_counts.items(), key=lambda x: x[1]) if daily_counts else None
}
except Exception as e:
self.logger.error(f"Error generating calendar summary: {str(e)}")
return {}

View File

@@ -0,0 +1,112 @@
from datetime import datetime
from typing import Dict, Any, Optional, List
from enum import Enum
from dataclasses import dataclass, field
from pydantic import BaseModel
class JobStatus(str, Enum):
"""Status of a scheduled job."""
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
MISSED = "missed"
class JobType(str, Enum):
"""Type of scheduled job."""
ONE_TIME = "one_time"
RECURRING = "recurring"
BATCH = "batch"
class JobPriority(int, Enum):
"""Priority of a scheduled job."""
LOW = 0
MEDIUM = 1
HIGH = 2
CRITICAL = 3
@dataclass
class JobMetadata:
"""Metadata for a scheduled job."""
retry_count: int = 0
max_retries: int = 3
retry_delay: int = 300 # seconds
priority: JobPriority = JobPriority.MEDIUM
tags: List[str] = field(default_factory=list)
custom_data: Dict[str, Any] = field(default_factory=dict)
class ScheduledJob(BaseModel):
"""Model for a scheduled job."""
job_id: str
content_id: str
schedule_type: JobType
status: JobStatus
platforms: List[str]
publish_date: datetime
created_at: datetime = field(default_factory=datetime.now)
updated_at: datetime = field(default_factory=datetime.now)
cron_expression: Optional[str] = None
end_date: Optional[datetime] = None
metadata: JobMetadata = field(default_factory=JobMetadata)
error: Optional[str] = None
last_run: Optional[datetime] = None
next_run: Optional[datetime] = None
class Config:
arbitrary_types_allowed = True
def to_dict(self) -> Dict[str, Any]:
"""Convert job to dictionary."""
return {
'job_id': self.job_id,
'content_id': self.content_id,
'schedule_type': self.schedule_type,
'status': self.status,
'platforms': self.platforms,
'publish_date': self.publish_date.isoformat(),
'created_at': self.created_at.isoformat(),
'updated_at': self.updated_at.isoformat(),
'cron_expression': self.cron_expression,
'end_date': self.end_date.isoformat() if self.end_date else None,
'metadata': {
'retry_count': self.metadata.retry_count,
'max_retries': self.metadata.max_retries,
'retry_delay': self.metadata.retry_delay,
'priority': self.metadata.priority,
'tags': self.metadata.tags,
'custom_data': self.metadata.custom_data
},
'error': self.error,
'last_run': self.last_run.isoformat() if self.last_run else None,
'next_run': self.next_run.isoformat() if self.next_run else None
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'ScheduledJob':
"""Create job from dictionary."""
metadata = JobMetadata(
retry_count=data['metadata']['retry_count'],
max_retries=data['metadata']['max_retries'],
retry_delay=data['metadata']['retry_delay'],
priority=data['metadata']['priority'],
tags=data['metadata']['tags'],
custom_data=data['metadata']['custom_data']
)
return cls(
job_id=data['job_id'],
content_id=data['content_id'],
schedule_type=data['schedule_type'],
status=data['status'],
platforms=data['platforms'],
publish_date=datetime.fromisoformat(data['publish_date']),
created_at=datetime.fromisoformat(data['created_at']),
updated_at=datetime.fromisoformat(data['updated_at']),
cron_expression=data.get('cron_expression'),
end_date=datetime.fromisoformat(data['end_date']) if data.get('end_date') else None,
metadata=metadata,
error=data.get('error'),
last_run=datetime.fromisoformat(data['last_run']) if data.get('last_run') else None,
next_run=datetime.fromisoformat(data['next_run']) if data.get('next_run') else None
)

View File

@@ -0,0 +1,15 @@
"""
Job status model for content scheduling.
"""
from enum import Enum
class JobStatus(str, Enum):
"""Enum representing the status of a scheduled job."""
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
RETRYING = "retrying"

View File

@@ -0,0 +1,153 @@
from datetime import datetime
from typing import Dict, Any, Optional, List
from enum import Enum
from dataclasses import dataclass, field
from pydantic import BaseModel, Field
class ScheduleType(str, Enum):
"""Type of schedule."""
ONE_TIME = "one_time"
RECURRING = "recurring"
BATCH = "batch"
class ScheduleStatus(str, Enum):
"""Status of a schedule."""
ACTIVE = "active"
PAUSED = "paused"
COMPLETED = "completed"
CANCELLED = "cancelled"
ERROR = "error"
@dataclass
class ScheduleMetadata:
"""Metadata for a schedule."""
description: Optional[str] = None
tags: List[str] = field(default_factory=list)
priority: int = 0
custom_data: Dict[str, Any] = field(default_factory=dict)
notification_settings: Dict[str, Any] = field(default_factory=dict)
class Schedule(BaseModel):
"""Model representing a content publishing schedule."""
content_id: str = Field(..., description="ID of the content to be published")
content: Dict[str, Any] = Field(..., description="Content to be published")
publish_date: datetime = Field(..., description="When to publish the content")
platforms: List[str] = Field(..., description="List of platforms to publish to")
schedule_type: str = Field(default="one_time", description="Type of schedule ('one_time' or 'recurring')")
cron_expression: Optional[str] = Field(None, description="Cron expression for recurring schedules")
end_date: Optional[datetime] = Field(None, description="End date for recurring schedules")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata for the schedule")
class Config:
"""Pydantic model configuration."""
arbitrary_types_allowed = True
def to_dict(self) -> Dict[str, Any]:
"""Convert schedule to dictionary."""
return {
'schedule_id': self.schedule_id,
'content_id': self.content_id,
'schedule_type': self.schedule_type,
'status': self.status,
'platforms': self.platforms,
'publish_date': self.publish_date.isoformat(),
'created_at': self.created_at.isoformat(),
'updated_at': self.updated_at.isoformat(),
'cron_expression': self.cron_expression,
'end_date': self.end_date.isoformat() if self.end_date else None,
'metadata': {
'description': self.metadata.description,
'tags': self.metadata.tags,
'priority': self.metadata.priority,
'custom_data': self.metadata.custom_data,
'notification_settings': self.metadata.notification_settings
},
'error': self.error,
'last_run': self.last_run.isoformat() if self.last_run else None,
'next_run': self.next_run.isoformat() if self.next_run else None,
'job_ids': self.job_ids
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'Schedule':
"""Create schedule from dictionary."""
metadata = ScheduleMetadata(
description=data['metadata'].get('description'),
tags=data['metadata'].get('tags', []),
priority=data['metadata'].get('priority', 0),
custom_data=data['metadata'].get('custom_data', {}),
notification_settings=data['metadata'].get('notification_settings', {})
)
return cls(
schedule_id=data['schedule_id'],
content_id=data['content_id'],
schedule_type=data['schedule_type'],
status=data['status'],
platforms=data['platforms'],
publish_date=datetime.fromisoformat(data['publish_date']),
created_at=datetime.fromisoformat(data['created_at']),
updated_at=datetime.fromisoformat(data['updated_at']),
cron_expression=data.get('cron_expression'),
end_date=datetime.fromisoformat(data['end_date']) if data.get('end_date') else None,
metadata=metadata,
error=data.get('error'),
last_run=datetime.fromisoformat(data['last_run']) if data.get('last_run') else None,
next_run=datetime.fromisoformat(data['next_run']) if data.get('next_run') else None,
job_ids=data.get('job_ids', [])
)
def is_active(self) -> bool:
"""Check if schedule is active."""
return self.status == ScheduleStatus.ACTIVE
def is_completed(self) -> bool:
"""Check if schedule is completed."""
return self.status == ScheduleStatus.COMPLETED
def is_cancelled(self) -> bool:
"""Check if schedule is cancelled."""
return self.status == ScheduleStatus.CANCELLED
def is_error(self) -> bool:
"""Check if schedule has error."""
return self.status == ScheduleStatus.ERROR
def is_recurring(self) -> bool:
"""Check if schedule is recurring."""
return self.schedule_type == ScheduleType.RECURRING
def is_one_time(self) -> bool:
"""Check if schedule is one-time."""
return self.schedule_type == ScheduleType.ONE_TIME
def is_batch(self) -> bool:
"""Check if schedule is batch."""
return self.schedule_type == ScheduleType.BATCH
def add_job_id(self, job_id: str):
"""Add a job ID to the schedule."""
if job_id not in self.job_ids:
self.job_ids.append(job_id)
def remove_job_id(self, job_id: str):
"""Remove a job ID from the schedule."""
if job_id in self.job_ids:
self.job_ids.remove(job_id)
def update_status(self, status: ScheduleStatus, error: Optional[str] = None):
"""Update schedule status."""
self.status = status
self.error = error
self.updated_at = datetime.now()
def update_next_run(self, next_run: datetime):
"""Update next run time."""
self.next_run = next_run
self.updated_at = datetime.now()
def update_last_run(self, last_run: datetime):
"""Update last run time."""
self.last_run = last_run
self.updated_at = datetime.now()

View File

@@ -0,0 +1,75 @@
"""
Timeline models for the Content Scheduler.
"""
from dataclasses import dataclass
from datetime import datetime
from typing import List, Dict, Any, Optional
from enum import Enum
class TimelineViewType(Enum):
"""Types of timeline views."""
GANTT = "gantt"
TIMELINE = "timeline"
LIST = "list"
class TimelineDependencyType(Enum):
"""Types of timeline dependencies."""
FINISH_TO_START = "finish_to_start"
START_TO_START = "start_to_start"
FINISH_TO_FINISH = "finish_to_finish"
START_TO_FINISH = "start_to_finish"
@dataclass
class TimelineDependency:
"""Timeline dependency model."""
source_id: str
target_id: str
dependency_type: TimelineDependencyType
lag: Optional[int] = None # Lag time in minutes
@dataclass
class TimelineTask:
"""Timeline task model."""
id: str
title: str
start_time: datetime
end_time: datetime
platform: str
status: str
progress: float
dependencies: List[TimelineDependency]
metadata: Dict[str, Any]
@dataclass
class TimelineMilestone:
"""Timeline milestone model."""
id: str
title: str
date: datetime
description: Optional[str] = None
status: str = "pending"
metadata: Dict[str, Any] = None
@dataclass
class TimelineView:
"""Timeline view model."""
view_type: TimelineViewType
start_date: datetime
end_date: datetime
tasks: List[TimelineTask]
milestones: List[TimelineMilestone]
dependencies: List[TimelineDependency]
metadata: Dict[str, Any]
@dataclass
class TimelineProgress:
"""Timeline progress model."""
total_tasks: int
completed_tasks: int
in_progress_tasks: int
pending_tasks: int
progress_percentage: float
by_platform: Dict[str, float]
by_date: Dict[str, float]
metadata: Dict[str, Any]

View File

@@ -0,0 +1,26 @@
APScheduler>=3.9.1
SQLAlchemy>=1.4.0
FastAPI>=0.68.0
Streamlit>=1.24.0
Pandas>=1.5.0
Plotly>=5.13.0
python-dateutil>=2.8.2
pytz>=2021.3
redis>=4.0.0
pydantic>=1.8.2
python-multipart>=0.0.5
aiohttp>=3.8.1
asyncio>=3.4.3
typing-extensions>=4.0.0
python-jose[cryptography]>=3.3.0
passlib[bcrypt]>=1.7.4
pytest>=6.2.5
pytest-asyncio>=0.16.0
pytest-cov>=2.12.1
black>=21.9b0
isort>=5.9.3
flake8>=3.9.2
mypy>=0.910
google-auth-oauthlib>=0.4.6
google-auth-httplib2>=0.1.0
google-api-python-client>=2.0.0

View File

@@ -0,0 +1,7 @@
"""
UI module for the Content Scheduler dashboard.
"""
from .dashboard import run_dashboard
__all__ = ['run_dashboard']

View File

@@ -0,0 +1,386 @@
"""
Main dashboard implementation for the Content Scheduler.
"""
import streamlit as st
import pandas as pd
from datetime import datetime, timedelta
from typing import List, Dict, Any
import plotly.express as px
import plotly.graph_objects as go
from lib.database.models import ContentItem, Schedule, ScheduleStatus, ContentType, Platform, get_engine, get_session, init_db
engine = get_engine()
init_db(engine)
session = get_session(engine)
def run_dashboard():
"""Run the Streamlit dashboard."""
st.title("📅 Alwrity Content Scheduler Dashboard")
# Sidebar navigation
st.sidebar.title("Navigation")
page = st.sidebar.radio(
"Go to",
["Overview", "Schedule Management", "Create Schedule", "Job Monitor", "Analytics"]
)
if page == "Overview":
show_overview()
elif page == "Schedule Management":
show_schedule_management()
elif page == "Create Schedule":
show_create_schedule()
elif page == "Job Monitor":
show_job_monitor()
else:
show_analytics()
def show_overview():
"""Display the overview dashboard."""
st.header("📊 Overview")
# Get data from unified database
all_content = session.query(ContentItem).all()
all_schedules = session.query(Schedule).all()
# Display metrics
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Total Content Items", len(all_content))
with col2:
scheduled_count = len([s for s in all_schedules if s.status == ScheduleStatus.SCHEDULED])
st.metric("Scheduled Items", scheduled_count)
with col3:
completed_count = len([s for s in all_schedules if s.status == ScheduleStatus.COMPLETED])
st.metric("Completed", completed_count)
with col4:
failed_count = len([s for s in all_schedules if s.status == ScheduleStatus.FAILED])
st.metric("Failed", failed_count)
# Recent content
st.subheader("📝 Recent Content Items")
if all_content:
recent_content = sorted(all_content, key=lambda x: x.created_at, reverse=True)[:5]
for item in recent_content:
with st.expander(f"{item.title} ({item.content_type.value})"):
st.write(f"**Description:** {item.description or 'No description'}")
st.write(f"**Platforms:** {', '.join(item.platforms) if isinstance(item.platforms, list) else item.platforms}")
st.write(f"**Status:** {item.status}")
st.write(f"**Created:** {item.created_at}")
# Show associated schedules
item_schedules = [s for s in all_schedules if s.content_item_id == item.id]
if item_schedules:
st.write("**Schedules:**")
for schedule in item_schedules:
st.write(f" - {schedule.scheduled_time} ({schedule.status.value})")
else:
st.info("No content items found. Create some content in the Content Calendar first!")
def show_schedule_management():
"""Display the schedule management interface."""
st.header("📅 Schedule Management")
# Get all schedules
all_schedules = session.query(Schedule).all()
if not all_schedules:
st.info("No schedules found. Create schedules from the 'Create Schedule' tab.")
return
# Filter options
col1, col2 = st.columns(2)
with col1:
status_filter = st.selectbox(
"Filter by Status",
options=["All"] + [status.value for status in ScheduleStatus],
key="schedule_status_filter"
)
with col2:
date_filter = st.date_input(
"Filter by Date (from)",
value=datetime.now().date() - timedelta(days=30),
key="schedule_date_filter"
)
# Apply filters
filtered_schedules = all_schedules
if status_filter != "All":
filtered_schedules = [s for s in filtered_schedules if s.status.value == status_filter]
filtered_schedules = [s for s in filtered_schedules if s.scheduled_time.date() >= date_filter]
# Display schedules
st.subheader(f"📋 Schedules ({len(filtered_schedules)} items)")
for schedule in sorted(filtered_schedules, key=lambda x: x.scheduled_time, reverse=True):
content_item = session.query(ContentItem).get(schedule.content_item_id)
if content_item:
with st.expander(f"{content_item.title} - {schedule.scheduled_time.strftime('%Y-%m-%d %H:%M')} ({schedule.status.value})"):
col1, col2 = st.columns(2)
with col1:
st.write(f"**Content:** {content_item.title}")
st.write(f"**Type:** {content_item.content_type.value}")
st.write(f"**Platforms:** {', '.join(content_item.platforms) if isinstance(content_item.platforms, list) else content_item.platforms}")
st.write(f"**Scheduled Time:** {schedule.scheduled_time}")
st.write(f"**Status:** {schedule.status.value}")
with col2:
st.write(f"**Recurrence:** {schedule.recurrence or 'One-time'}")
st.write(f"**Priority:** {schedule.priority}")
st.write(f"**Created:** {schedule.created_at}")
if schedule.result:
st.write(f"**Result:** {schedule.result}")
# Action buttons
col1, col2, col3 = st.columns(3)
with col1:
if st.button(f"Edit Schedule", key=f"edit_{schedule.id}"):
st.session_state.edit_schedule_id = schedule.id
st.rerun()
with col2:
if schedule.status == ScheduleStatus.SCHEDULED:
if st.button(f"Cancel", key=f"cancel_{schedule.id}"):
schedule.status = ScheduleStatus.CANCELLED
session.commit()
st.success("Schedule cancelled!")
st.rerun()
with col3:
if st.button(f"Delete", key=f"delete_{schedule.id}"):
session.delete(schedule)
session.commit()
st.success("Schedule deleted!")
st.rerun()
def show_create_schedule():
"""Display the schedule creation interface."""
st.header(" Create New Schedule")
# Get available content items
content_items = session.query(ContentItem).all()
if not content_items:
st.warning("No content items available. Please create content in the Content Calendar first.")
return
# Create schedule form
with st.form("create_schedule_form"):
st.subheader("Schedule Configuration")
# Select content item
content_options = {f"{item.title} ({item.content_type.value})": item.id for item in content_items}
selected_content = st.selectbox(
"Select Content Item",
options=list(content_options.keys()),
key="schedule_content_select"
)
# Schedule timing
col1, col2 = st.columns(2)
with col1:
schedule_date = st.date_input(
"Schedule Date",
value=datetime.now().date() + timedelta(days=1),
key="schedule_date"
)
with col2:
schedule_time = st.time_input(
"Schedule Time",
value=datetime.now().time(),
key="schedule_time"
)
# Combine date and time
schedule_datetime = datetime.combine(schedule_date, schedule_time)
# Recurrence options
recurrence = st.selectbox(
"Recurrence",
options=["none", "daily", "weekly", "monthly"],
key="schedule_recurrence"
)
# Priority
priority = st.slider(
"Priority",
min_value=1,
max_value=10,
value=5,
key="schedule_priority"
)
# Platform selection (override content item platforms if needed)
content_item_id = content_options[selected_content]
content_item = session.query(ContentItem).get(content_item_id)
if content_item:
current_platforms = content_item.platforms if isinstance(content_item.platforms, list) else [content_item.platforms]
st.write(f"**Current Platforms:** {', '.join(current_platforms)}")
override_platforms = st.checkbox("Override Platforms", key="override_platforms")
if override_platforms:
available_platforms = [p.value for p in Platform]
selected_platforms = st.multiselect(
"Select Platforms",
options=available_platforms,
default=current_platforms,
key="schedule_platforms"
)
else:
selected_platforms = current_platforms
# Submit button
submitted = st.form_submit_button("Create Schedule")
if submitted:
try:
# Create new schedule
new_schedule = Schedule(
content_item_id=content_item_id,
scheduled_time=schedule_datetime,
status=ScheduleStatus.SCHEDULED,
recurrence=recurrence if recurrence != "none" else None,
priority=priority
)
session.add(new_schedule)
session.commit()
st.success(f"✅ Schedule created successfully! Content will be published on {schedule_datetime}")
# Show schedule details
with st.expander("Schedule Details", expanded=True):
st.write(f"**Content:** {content_item.title}")
st.write(f"**Scheduled Time:** {schedule_datetime}")
st.write(f"**Platforms:** {', '.join(selected_platforms)}")
st.write(f"**Recurrence:** {recurrence}")
st.write(f"**Priority:** {priority}")
except Exception as e:
st.error(f"❌ Error creating schedule: {str(e)}")
def show_job_monitor():
"""Display the job monitoring interface."""
st.header("🔍 Job Monitor")
# Get all schedules with their status
all_schedules = session.query(Schedule).all()
if not all_schedules:
st.info("No jobs to monitor.")
return
# Status distribution
status_counts = {}
for schedule in all_schedules:
status = schedule.status.value
status_counts[status] = status_counts.get(status, 0) + 1
# Display status chart
if status_counts:
fig = px.pie(
values=list(status_counts.values()),
names=list(status_counts.keys()),
title="Job Status Distribution"
)
st.plotly_chart(fig, use_container_width=True)
# Recent job activity
st.subheader("📊 Recent Job Activity")
recent_schedules = sorted(all_schedules, key=lambda x: x.updated_at, reverse=True)[:10]
for schedule in recent_schedules:
content_item = session.query(ContentItem).get(schedule.content_item_id)
if content_item:
status_color = {
ScheduleStatus.SCHEDULED: "🟡",
ScheduleStatus.RUNNING: "🔵",
ScheduleStatus.COMPLETED: "🟢",
ScheduleStatus.FAILED: "🔴",
ScheduleStatus.CANCELLED: ""
}.get(schedule.status, "")
st.write(f"{status_color} **{content_item.title}** - {schedule.status.value} - {schedule.updated_at.strftime('%Y-%m-%d %H:%M')}")
if schedule.result:
st.write(f" └─ {schedule.result}")
def show_analytics():
"""Display the analytics dashboard."""
st.header("📈 Analytics")
# Get data
all_content = session.query(ContentItem).all()
all_schedules = session.query(Schedule).all()
if not all_schedules:
st.info("No data available for analytics.")
return
# Time-based analytics
st.subheader("📅 Schedule Timeline")
# Create timeline data
timeline_data = []
for schedule in all_schedules:
content_item = session.query(ContentItem).get(schedule.content_item_id)
if content_item:
timeline_data.append({
'Date': schedule.scheduled_time.date(),
'Content': content_item.title,
'Status': schedule.status.value,
'Type': content_item.content_type.value
})
if timeline_data:
df = pd.DataFrame(timeline_data)
# Schedule frequency by date
date_counts = df.groupby('Date').size().reset_index(name='Count')
fig = px.line(date_counts, x='Date', y='Count', title='Scheduled Content Over Time')
st.plotly_chart(fig, use_container_width=True)
# Content type distribution
type_counts = df['Type'].value_counts()
fig = px.bar(x=type_counts.index, y=type_counts.values, title='Content Type Distribution')
st.plotly_chart(fig, use_container_width=True)
# Status breakdown
status_counts = df['Status'].value_counts()
fig = px.pie(values=status_counts.values, names=status_counts.index, title='Status Distribution')
st.plotly_chart(fig, use_container_width=True)
# Performance metrics
st.subheader("📊 Performance Metrics")
col1, col2, col3 = st.columns(3)
with col1:
total_schedules = len(all_schedules)
st.metric("Total Schedules", total_schedules)
with col2:
completed_schedules = len([s for s in all_schedules if s.status == ScheduleStatus.COMPLETED])
success_rate = (completed_schedules / total_schedules * 100) if total_schedules > 0 else 0
st.metric("Success Rate", f"{success_rate:.1f}%")
with col3:
failed_schedules = len([s for s in all_schedules if s.status == ScheduleStatus.FAILED])
failure_rate = (failed_schedules / total_schedules * 100) if total_schedules > 0 else 0
st.metric("Failure Rate", f"{failure_rate:.1f}%")

View File

@@ -0,0 +1,392 @@
"""
Timeline view implementation for the Content Scheduler.
Provides interactive Gantt charts and progress tracking visualization.
"""
import streamlit as st
import plotly.figure_factory as ff
import plotly.graph_objects as go
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
import pandas as pd
import json
# Use unified database models
from lib.database.models import ContentItem, Schedule, ScheduleStatus, get_session
class TimelineView:
"""Interactive timeline view with Gantt charts and progress tracking."""
def __init__(self):
"""Initialize the timeline view."""
self.session = get_session()
def render(self):
"""Render the timeline view."""
st.header("Schedule Timeline")
# Timeline controls
self._render_timeline_controls()
# Main timeline view
self._render_timeline()
# Progress tracking
self._render_progress_tracking()
def _render_timeline_controls(self):
"""Render timeline control options."""
col1, col2, col3 = st.columns([2, 2, 1])
with col1:
view_type = st.selectbox(
"View Type",
["Gantt Chart", "Timeline", "List View"],
help="Select the type of timeline visualization"
)
with col2:
date_range = st.date_input(
"Date Range",
value=(
datetime.now().date(),
datetime.now().date() + timedelta(days=7)
),
help="Select the date range to display"
)
with col3:
if st.button("Export", help="Export timeline data"):
self._export_timeline_data()
def _render_timeline(self):
"""Render the main timeline visualization."""
# Get schedules for the selected date range
schedules = self._get_schedules_for_timeline()
if not schedules:
st.info("No schedules found for the selected date range.")
return
# Create Gantt chart data
gantt_data = self._create_gantt_data(schedules)
# Create and display Gantt chart
fig = self._create_gantt_chart(gantt_data)
st.plotly_chart(fig, use_container_width=True)
# Display schedule details
self._render_schedule_details(schedules)
def _render_progress_tracking(self):
"""Render progress tracking visualization."""
st.subheader("Progress Tracking")
# Progress metrics
col1, col2, col3 = st.columns(3)
with col1:
self._render_progress_metric(
"Completed",
self._get_completed_count(),
"green"
)
with col2:
self._render_progress_metric(
"In Progress",
self._get_in_progress_count(),
"orange"
)
with col3:
self._render_progress_metric(
"Pending",
self._get_pending_count(),
"blue"
)
# Progress chart
self._render_progress_chart()
def _get_schedules_for_timeline(self) -> List[Schedule]:
"""Get schedules for the timeline view."""
try:
# Get date range from session state or use default
if hasattr(st.session_state, 'date_range') and st.session_state.date_range:
start_date, end_date = st.session_state.date_range
else:
start_date = datetime.now().date()
end_date = start_date + timedelta(days=7)
# Convert to datetime
start_datetime = datetime.combine(start_date, datetime.min.time())
end_datetime = datetime.combine(end_date, datetime.max.time())
# Query schedules from unified database
schedules = self.session.query(Schedule).filter(
Schedule.scheduled_time >= start_datetime,
Schedule.scheduled_time <= end_datetime
).all()
return schedules
except Exception as e:
st.error(f"Failed to get schedules: {str(e)}")
return []
def _create_gantt_data(self, schedules: List[Schedule]) -> List[Dict[str, Any]]:
"""Create data for Gantt chart."""
gantt_data = []
for schedule in schedules:
# Get content item details
content_item = self.session.query(ContentItem).filter(
ContentItem.id == schedule.content_item_id
).first()
if content_item:
# Calculate task duration
duration = timedelta(hours=1) # Default duration
# Create task data
task = {
'Task': content_item.title[:50] + "..." if len(content_item.title) > 50 else content_item.title,
'Start': schedule.scheduled_time,
'Finish': schedule.scheduled_time + duration,
'Resource': schedule.status.value,
'Status': schedule.status.value,
'Progress': self._calculate_progress(schedule)
}
gantt_data.append(task)
return gantt_data
def _create_gantt_chart(self, gantt_data: List[Dict[str, Any]]) -> go.Figure:
"""Create Gantt chart visualization."""
if not gantt_data:
# Return empty figure
fig = go.Figure()
fig.update_layout(
title='Content Schedule Timeline',
xaxis_title='Timeline',
yaxis_title='Status',
height=400
)
return fig
# Convert data to DataFrame
df = pd.DataFrame(gantt_data)
# Create Gantt chart
fig = ff.create_gantt(
df,
index_col='Resource',
show_colorbar=True,
group_tasks=True,
showgrid_x=True,
showgrid_y=True
)
# Update layout
fig.update_layout(
title='Content Schedule Timeline',
xaxis_title='Timeline',
yaxis_title='Status',
height=400,
showlegend=True
)
return fig
def _render_schedule_details(self, schedules: List[Schedule]):
"""Render detailed schedule information."""
st.subheader("Schedule Details")
for schedule in schedules:
# Get content item details
content_item = self.session.query(ContentItem).filter(
ContentItem.id == schedule.content_item_id
).first()
if content_item:
with st.expander(f"{content_item.title} - {schedule.status.value}"):
col1, col2 = st.columns(2)
with col1:
st.write("**Schedule Information**")
st.write(f"Content Type: {content_item.content_type.value if content_item.content_type else 'Unknown'}")
st.write(f"Status: {schedule.status.value}")
st.write(f"Scheduled Time: {schedule.scheduled_time}")
st.write(f"Priority: {schedule.priority}")
if schedule.recurrence:
st.write(f"Recurrence: {schedule.recurrence}")
with col2:
st.write("**Progress**")
progress = self._calculate_progress(schedule)
st.progress(progress / 100)
st.write(f"Progress: {progress:.1f}%")
# Action buttons
col2a, col2b = st.columns(2)
with col2a:
if st.button(f"Edit {schedule.id}", key=f"edit_{schedule.id}"):
st.session_state.edit_schedule_id = schedule.id
with col2b:
if st.button(f"Cancel {schedule.id}", key=f"cancel_{schedule.id}"):
self._cancel_schedule(schedule.id)
def _render_progress_metric(self, label: str, value: int, color: str):
"""Render a progress metric."""
st.metric(label, value)
def _render_progress_chart(self):
"""Render progress chart visualization."""
try:
# Get progress data
progress_data = self._get_progress_data()
if progress_data:
# Create pie chart
labels = list(progress_data.keys())
values = list(progress_data.values())
fig = go.Figure(data=[go.Pie(labels=labels, values=values)])
fig.update_layout(
title="Schedule Status Distribution",
height=300
)
st.plotly_chart(fig, use_container_width=True)
else:
st.info("No progress data available.")
except Exception as e:
st.error(f"Error rendering progress chart: {str(e)}")
def _calculate_progress(self, schedule: Schedule) -> float:
"""Calculate progress percentage for a schedule."""
try:
if schedule.status == ScheduleStatus.COMPLETED:
return 100.0
elif schedule.status == ScheduleStatus.RUNNING:
return 50.0
elif schedule.status == ScheduleStatus.FAILED:
return 0.0
else: # PENDING
return 0.0
except Exception as e:
st.error(f"Error calculating progress: {str(e)}")
return 0.0
def _get_completed_count(self) -> int:
"""Get count of completed schedules."""
try:
return self.session.query(Schedule).filter(
Schedule.status == ScheduleStatus.COMPLETED
).count()
except Exception as e:
st.error(f"Error getting completed count: {str(e)}")
return 0
def _get_in_progress_count(self) -> int:
"""Get count of in-progress schedules."""
try:
return self.session.query(Schedule).filter(
Schedule.status == ScheduleStatus.RUNNING
).count()
except Exception as e:
st.error(f"Error getting in-progress count: {str(e)}")
return 0
def _get_pending_count(self) -> int:
"""Get count of pending schedules."""
try:
return self.session.query(Schedule).filter(
Schedule.status == ScheduleStatus.PENDING
).count()
except Exception as e:
st.error(f"Error getting pending count: {str(e)}")
return 0
def _get_progress_data(self) -> Dict[str, int]:
"""Get progress data for visualization."""
try:
progress_data = {}
# Count schedules by status
for status in ScheduleStatus:
count = self.session.query(Schedule).filter(
Schedule.status == status
).count()
progress_data[status.value] = count
return progress_data
except Exception as e:
st.error(f"Error getting progress data: {str(e)}")
return {}
def _cancel_schedule(self, schedule_id: int):
"""Cancel a schedule."""
try:
schedule = self.session.query(Schedule).filter(
Schedule.id == schedule_id
).first()
if schedule:
schedule.status = ScheduleStatus.CANCELLED
self.session.commit()
st.success(f"Schedule {schedule_id} cancelled successfully!")
st.experimental_rerun()
else:
st.error("Schedule not found.")
except Exception as e:
st.error(f"Error cancelling schedule: {str(e)}")
self.session.rollback()
def _export_timeline_data(self):
"""Export timeline data."""
try:
schedules = self._get_schedules_for_timeline()
if schedules:
# Prepare export data
export_data = []
for schedule in schedules:
content_item = self.session.query(ContentItem).filter(
ContentItem.id == schedule.content_item_id
).first()
if content_item:
export_data.append({
'Schedule ID': schedule.id,
'Title': content_item.title,
'Content Type': content_item.content_type.value if content_item.content_type else 'Unknown',
'Scheduled Time': schedule.scheduled_time.isoformat(),
'Status': schedule.status.value,
'Priority': schedule.priority,
'Recurrence': schedule.recurrence or 'None'
})
# Convert to CSV
df = pd.DataFrame(export_data)
csv = df.to_csv(index=False)
# Provide download
st.download_button(
label="Download Timeline Data",
data=csv,
file_name=f"timeline_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
mime="text/csv"
)
else:
st.warning("No data to export.")
except Exception as e:
st.error(f"Error exporting data: {str(e)}")

View File

@@ -0,0 +1,201 @@
from typing import List, Optional, Dict, Any
from datetime import datetime, timedelta
import pytz
from dateutil import rrule
from .error_handling import ScheduleValidationError
def get_optimal_publish_time(
platform: str,
content_type: str,
target_audience: Optional[Dict[str, Any]] = None
) -> datetime:
"""Calculate optimal publish time based on platform and content type."""
now = datetime.now(pytz.UTC)
# Default optimal times by platform and content type
optimal_times = {
'TWITTER': {
'POST': {'hour': 12, 'minute': 0}, # Noon UTC
'THREAD': {'hour': 15, 'minute': 0}, # 3 PM UTC
'POLL': {'hour': 18, 'minute': 0}, # 6 PM UTC
},
'FACEBOOK': {
'POST': {'hour': 15, 'minute': 0}, # 3 PM UTC
'LIVE': {'hour': 19, 'minute': 0}, # 7 PM UTC
'EVENT': {'hour': 10, 'minute': 0}, # 10 AM UTC
},
'LINKEDIN': {
'POST': {'hour': 9, 'minute': 0}, # 9 AM UTC
'ARTICLE': {'hour': 11, 'minute': 0}, # 11 AM UTC
'POLL': {'hour': 14, 'minute': 0}, # 2 PM UTC
},
'INSTAGRAM': {
'POST': {'hour': 17, 'minute': 0}, # 5 PM UTC
'STORY': {'hour': 20, 'minute': 0}, # 8 PM UTC
'REEL': {'hour': 21, 'minute': 0}, # 9 PM UTC
}
}
if platform not in optimal_times:
raise ScheduleValidationError(
f"Unsupported platform: {platform}",
{'supported_platforms': list(optimal_times.keys())}
)
if content_type not in optimal_times[platform]:
raise ScheduleValidationError(
f"Unsupported content type for {platform}: {content_type}",
{'supported_types': list(optimal_times[platform].keys())}
)
optimal_time = optimal_times[platform][content_type]
publish_time = now.replace(
hour=optimal_time['hour'],
minute=optimal_time['minute'],
second=0,
microsecond=0
)
# If the optimal time has passed for today, schedule for tomorrow
if publish_time < now:
publish_time += timedelta(days=1)
return publish_time
def calculate_recurrence_dates(
start_date: datetime,
frequency: str,
interval: int,
end_date: Optional[datetime] = None,
count: Optional[int] = None
) -> List[datetime]:
"""Calculate recurrence dates based on frequency and interval."""
if not isinstance(start_date, datetime):
raise ScheduleValidationError(
"Start date must be a datetime object",
{'type': type(start_date).__name__}
)
if start_date.tzinfo is None:
raise ScheduleValidationError(
"Start date must be timezone-aware",
{'date': str(start_date)}
)
frequency_map = {
'DAILY': rrule.DAILY,
'WEEKLY': rrule.WEEKLY,
'MONTHLY': rrule.MONTHLY,
'YEARLY': rrule.YEARLY
}
if frequency not in frequency_map:
raise ScheduleValidationError(
f"Invalid frequency: {frequency}",
{'valid_frequencies': list(frequency_map.keys())}
)
if not isinstance(interval, int) or interval < 1:
raise ScheduleValidationError(
"Interval must be a positive integer",
{'interval': interval}
)
if end_date is not None and not isinstance(end_date, datetime):
raise ScheduleValidationError(
"End date must be a datetime object",
{'type': type(end_date).__name__}
)
if end_date is not None and end_date.tzinfo is None:
raise ScheduleValidationError(
"End date must be timezone-aware",
{'date': str(end_date)}
)
if count is not None and (not isinstance(count, int) or count < 1):
raise ScheduleValidationError(
"Count must be a positive integer",
{'count': count}
)
rule = rrule.rrule(
freq=frequency_map[frequency],
interval=interval,
dtstart=start_date,
until=end_date,
count=count
)
return list(rule)
def adjust_for_timezone(
date: datetime,
target_timezone: str
) -> datetime:
"""Adjust datetime to target timezone."""
if not isinstance(date, datetime):
raise ScheduleValidationError(
"Date must be a datetime object",
{'type': type(date).__name__}
)
if date.tzinfo is None:
raise ScheduleValidationError(
"Date must be timezone-aware",
{'date': str(date)}
)
try:
target_tz = pytz.timezone(target_timezone)
except pytz.exceptions.UnknownTimeZoneError:
raise ScheduleValidationError(
f"Invalid timezone: {target_timezone}",
{'timezone': target_timezone}
)
return date.astimezone(target_tz)
def calculate_time_difference(
date1: datetime,
date2: datetime
) -> timedelta:
"""Calculate time difference between two dates."""
if not isinstance(date1, datetime) or not isinstance(date2, datetime):
raise ScheduleValidationError(
"Both dates must be datetime objects",
{
'date1_type': type(date1).__name__,
'date2_type': type(date2).__name__
}
)
if date1.tzinfo is None or date2.tzinfo is None:
raise ScheduleValidationError(
"Both dates must be timezone-aware",
{
'date1': str(date1),
'date2': str(date2)
}
)
return date2 - date1
def format_date_for_display(
date: datetime,
format_str: str = "%Y-%m-%d %H:%M:%S %Z"
) -> str:
"""Format datetime for display."""
if not isinstance(date, datetime):
raise ScheduleValidationError(
"Date must be a datetime object",
{'type': type(date).__name__}
)
if date.tzinfo is None:
raise ScheduleValidationError(
"Date must be timezone-aware",
{'date': str(date)}
)
return date.strftime(format_str)

View File

@@ -0,0 +1,134 @@
from typing import Optional, Dict, Any
import logging
from functools import wraps
import traceback
logger = logging.getLogger('content_scheduler')
class SchedulingError(Exception):
"""Exception raised for errors in content scheduling."""
def __init__(self, message: str):
"""Initialize the error with a message.
Args:
message: Error message
"""
self.message = message
super().__init__(self.message)
class JobExecutionError(SchedulingError):
"""Exception for job execution errors."""
pass
class ScheduleValidationError(SchedulingError):
"""Exception for schedule validation errors."""
pass
class PlatformError(SchedulingError):
"""Exception for platform-specific errors."""
pass
class DatabaseError(SchedulingError):
"""Exception for database-related errors."""
pass
def handle_scheduler_error(func):
"""Decorator for handling scheduler errors."""
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except SchedulingError as e:
logger.error(f"Scheduling error in {func.__name__}: {str(e)}")
raise
except Exception as e:
logger.error(f"Unexpected error in {func.__name__}: {str(e)}")
logger.error(traceback.format_exc())
raise SchedulingError(
f"Unexpected error in {func.__name__}: {str(e)}",
{'traceback': traceback.format_exc()}
)
return wrapper
def handle_job_error(func):
"""Decorator for handling job execution errors."""
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
logger.error(f"Job execution error in {func.__name__}: {str(e)}")
logger.error(traceback.format_exc())
raise JobExecutionError(
f"Job execution failed: {str(e)}",
{
'function': func.__name__,
'traceback': traceback.format_exc()
}
)
return wrapper
def handle_platform_error(func):
"""Decorator for handling platform-specific errors."""
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
logger.error(f"Platform error in {func.__name__}: {str(e)}")
logger.error(traceback.format_exc())
raise PlatformError(
f"Platform operation failed: {str(e)}",
{
'function': func.__name__,
'traceback': traceback.format_exc()
}
)
return wrapper
def handle_database_error(func):
"""Decorator for handling database errors."""
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
logger.error(f"Database error in {func.__name__}: {str(e)}")
logger.error(traceback.format_exc())
raise DatabaseError(
f"Database operation failed: {str(e)}",
{
'function': func.__name__,
'traceback': traceback.format_exc()
}
)
return wrapper
def format_error(error: Exception) -> Dict[str, Any]:
"""Format error for logging and reporting."""
if isinstance(error, SchedulingError):
return {
'type': error.__class__.__name__,
'message': str(error),
'details': error.details
}
else:
return {
'type': 'UnexpectedError',
'message': str(error),
'details': {
'traceback': traceback.format_exc()
}
}
def log_error(error: Exception, context: Optional[Dict[str, Any]] = None):
"""Log error with context."""
error_data = format_error(error)
if context:
error_data['context'] = context
logger.error(
f"Error: {error_data['type']} - {error_data['message']}",
extra={'error_data': error_data}
)

View File

@@ -0,0 +1,11 @@
import logging
def setup_logger(name: str = "content_scheduler", level=logging.INFO):
logger = logging.getLogger(name)
if not logger.handlers:
handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(level)
return logger

View File

@@ -0,0 +1,285 @@
from typing import Dict, Any, List, Optional
import logging
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import aiohttp
import json
from .error_handling import PlatformError
logger = logging.getLogger('content_scheduler')
class NotificationManager:
"""Manages notifications for scheduled content."""
def __init__(self, config: Dict[str, Any]):
"""Initialize notification manager with configuration."""
self.config = config
self.email_config = config.get('email', {})
self.slack_config = config.get('slack', {})
self.webhook_config = config.get('webhook', {})
async def send_notification(
self,
event_type: str,
content: Dict[str, Any],
channels: List[str],
metadata: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Send notification through specified channels."""
results = {}
for channel in channels:
try:
if channel == 'EMAIL':
results['email'] = await self._send_email_notification(
event_type, content, metadata
)
elif channel == 'SLACK':
results['slack'] = await self._send_slack_notification(
event_type, content, metadata
)
elif channel == 'WEBHOOK':
results['webhook'] = await self._send_webhook_notification(
event_type, content, metadata
)
else:
logger.warning(f"Unsupported notification channel: {channel}")
except Exception as e:
logger.error(f"Failed to send {channel} notification: {str(e)}")
results[channel] = {
'success': False,
'error': str(e)
}
return results
async def _send_email_notification(
self,
event_type: str,
content: Dict[str, Any],
metadata: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Send email notification."""
if not self.email_config:
raise PlatformError(
"Email configuration not found",
{'event_type': event_type}
)
try:
msg = MIMEMultipart()
msg['From'] = self.email_config['from_email']
msg['To'] = self.email_config['to_email']
msg['Subject'] = self._get_email_subject(event_type, content)
body = self._format_email_body(event_type, content, metadata)
msg.attach(MIMEText(body, 'html'))
with smtplib.SMTP(
self.email_config['smtp_server'],
self.email_config['smtp_port']
) as server:
if self.email_config.get('use_tls'):
server.starttls()
if self.email_config.get('username'):
server.login(
self.email_config['username'],
self.email_config['password']
)
server.send_message(msg)
return {'success': True}
except Exception as e:
raise PlatformError(
f"Failed to send email notification: {str(e)}",
{
'event_type': event_type,
'error': str(e)
}
)
async def _send_slack_notification(
self,
event_type: str,
content: Dict[str, Any],
metadata: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Send Slack notification."""
if not self.slack_config:
raise PlatformError(
"Slack configuration not found",
{'event_type': event_type}
)
try:
message = self._format_slack_message(event_type, content, metadata)
async with aiohttp.ClientSession() as session:
async with session.post(
self.slack_config['webhook_url'],
json=message
) as response:
if response.status != 200:
raise PlatformError(
f"Slack API returned status {response.status}",
{'response': await response.text()}
)
return {'success': True}
except Exception as e:
raise PlatformError(
f"Failed to send Slack notification: {str(e)}",
{
'event_type': event_type,
'error': str(e)
}
)
async def _send_webhook_notification(
self,
event_type: str,
content: Dict[str, Any],
metadata: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Send webhook notification."""
if not self.webhook_config:
raise PlatformError(
"Webhook configuration not found",
{'event_type': event_type}
)
try:
payload = self._format_webhook_payload(event_type, content, metadata)
async with aiohttp.ClientSession() as session:
async with session.post(
self.webhook_config['url'],
json=payload,
headers=self.webhook_config.get('headers', {})
) as response:
if response.status != 200:
raise PlatformError(
f"Webhook returned status {response.status}",
{'response': await response.text()}
)
return {'success': True}
except Exception as e:
raise PlatformError(
f"Failed to send webhook notification: {str(e)}",
{
'event_type': event_type,
'error': str(e)
}
)
def _get_email_subject(
self,
event_type: str,
content: Dict[str, Any]
) -> str:
"""Generate email subject based on event type."""
subjects = {
'ON_SUCCESS': f"Content Published Successfully: {content.get('title', 'Untitled')}",
'ON_FAILURE': f"Content Publication Failed: {content.get('title', 'Untitled')}",
'ON_RETRY': f"Content Publication Retry: {content.get('title', 'Untitled')}",
'ON_CANCELLATION': f"Content Publication Cancelled: {content.get('title', 'Untitled')}"
}
return subjects.get(event_type, f"Content Update: {content.get('title', 'Untitled')}")
def _format_email_body(
self,
event_type: str,
content: Dict[str, Any],
metadata: Optional[Dict[str, Any]] = None
) -> str:
"""Format email body."""
template = f"""
<html>
<body>
<h2>Content Update Notification</h2>
<p><strong>Event Type:</strong> {event_type}</p>
<p><strong>Content Title:</strong> {content.get('title', 'Untitled')}</p>
<p><strong>Platform:</strong> {content.get('platform', 'Unknown')}</p>
<p><strong>Status:</strong> {content.get('status', 'Unknown')}</p>
"""
if metadata:
template += "<h3>Additional Details:</h3><ul>"
for key, value in metadata.items():
template += f"<li><strong>{key}:</strong> {value}</li>"
template += "</ul>"
template += """
</body>
</html>
"""
return template
def _format_slack_message(
self,
event_type: str,
content: Dict[str, Any],
metadata: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Format Slack message."""
message = {
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": self._get_email_subject(event_type, content)
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": f"*Event Type:*\n{event_type}"
},
{
"type": "mrkdwn",
"text": f"*Platform:*\n{content.get('platform', 'Unknown')}"
},
{
"type": "mrkdwn",
"text": f"*Status:*\n{content.get('status', 'Unknown')}"
}
]
}
]
}
if metadata:
fields = []
for key, value in metadata.items():
fields.append({
"type": "mrkdwn",
"text": f"*{key}:*\n{value}"
})
message["blocks"].append({
"type": "section",
"fields": fields
})
return message
def _format_webhook_payload(
self,
event_type: str,
content: Dict[str, Any],
metadata: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Format webhook payload."""
payload = {
'event_type': event_type,
'content': content,
'timestamp': datetime.now(pytz.UTC).isoformat()
}
if metadata:
payload['metadata'] = metadata
return payload

View File

@@ -0,0 +1,381 @@
"""
Timeline utilities for content scheduling.
"""
import logging
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional, Tuple
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
# Use unified database models
from lib.database.models import ContentItem, Schedule, ScheduleStatus
logger = logging.getLogger(__name__)
class TimelineAnalyzer:
"""Analyze and visualize content scheduling timelines."""
def __init__(self):
"""Initialize the timeline analyzer."""
self.logger = logger
def analyze_schedule_distribution(
self,
schedules: List[Schedule],
time_range: str = "week"
) -> Dict[str, Any]:
"""Analyze the distribution of schedules over time.
Args:
schedules: List of Schedule objects
time_range: Time range for analysis ('day', 'week', 'month')
Returns:
Dictionary containing analysis results
"""
try:
if not schedules:
return {
'total_schedules': 0,
'distribution': {},
'peak_times': [],
'gaps': []
}
# Group schedules by time period
distribution = {}
for schedule in schedules:
if time_range == "day":
key = schedule.scheduled_time.strftime("%Y-%m-%d")
elif time_range == "week":
# Get week start (Monday)
week_start = schedule.scheduled_time - timedelta(days=schedule.scheduled_time.weekday())
key = week_start.strftime("%Y-%m-%d")
else: # month
key = schedule.scheduled_time.strftime("%Y-%m")
distribution[key] = distribution.get(key, 0) + 1
# Find peak times
peak_times = sorted(distribution.items(), key=lambda x: x[1], reverse=True)[:3]
# Find gaps (periods with no content)
gaps = self._find_gaps(schedules, time_range)
return {
'total_schedules': len(schedules),
'distribution': distribution,
'peak_times': peak_times,
'gaps': gaps
}
except Exception as e:
self.logger.error(f"Error analyzing schedule distribution: {str(e)}")
return {}
def _find_gaps(
self,
schedules: List[Schedule],
time_range: str
) -> List[str]:
"""Find gaps in the schedule timeline.
Args:
schedules: List of Schedule objects
time_range: Time range for analysis
Returns:
List of time periods with no scheduled content
"""
try:
if not schedules:
return []
# Get date range
dates = [s.scheduled_time.date() for s in schedules]
start_date = min(dates)
end_date = max(dates)
# Generate all periods in range
current_date = start_date
all_periods = set()
while current_date <= end_date:
if time_range == "day":
period = current_date.strftime("%Y-%m-%d")
current_date += timedelta(days=1)
elif time_range == "week":
# Get week start (Monday)
week_start = current_date - timedelta(days=current_date.weekday())
period = week_start.strftime("%Y-%m-%d")
current_date += timedelta(weeks=1)
else: # month
period = current_date.strftime("%Y-%m")
# Move to next month
if current_date.month == 12:
current_date = current_date.replace(year=current_date.year + 1, month=1)
else:
current_date = current_date.replace(month=current_date.month + 1)
all_periods.add(period)
# Find periods with schedules
scheduled_periods = set()
for schedule in schedules:
if time_range == "day":
period = schedule.scheduled_time.strftime("%Y-%m-%d")
elif time_range == "week":
week_start = schedule.scheduled_time - timedelta(days=schedule.scheduled_time.weekday())
period = week_start.strftime("%Y-%m-%d")
else: # month
period = schedule.scheduled_time.strftime("%Y-%m")
scheduled_periods.add(period)
# Return gaps
gaps = list(all_periods - scheduled_periods)
return sorted(gaps)
except Exception as e:
self.logger.error(f"Error finding gaps: {str(e)}")
return []
def create_timeline_chart(
self,
schedules: List[Schedule],
chart_type: str = "gantt"
) -> go.Figure:
"""Create a timeline visualization chart.
Args:
schedules: List of Schedule objects
chart_type: Type of chart ('gantt', 'scatter', 'bar')
Returns:
Plotly figure object
"""
try:
if not schedules:
fig = go.Figure()
fig.add_annotation(
text="No schedules to display",
xref="paper", yref="paper",
x=0.5, y=0.5,
showarrow=False
)
return fig
if chart_type == "gantt":
return self._create_gantt_chart(schedules)
elif chart_type == "scatter":
return self._create_scatter_chart(schedules)
else: # bar
return self._create_bar_chart(schedules)
except Exception as e:
self.logger.error(f"Error creating timeline chart: {str(e)}")
fig = go.Figure()
fig.add_annotation(
text=f"Error creating chart: {str(e)}",
xref="paper", yref="paper",
x=0.5, y=0.5,
showarrow=False
)
return fig
def _create_gantt_chart(self, schedules: List[Schedule]) -> go.Figure:
"""Create a Gantt chart for schedules."""
try:
# Prepare data for Gantt chart
data = []
for i, schedule in enumerate(schedules):
# Estimate duration (default 1 hour)
start_time = schedule.scheduled_time
end_time = start_time + timedelta(hours=1)
data.append({
'Task': f"Schedule {schedule.id}",
'Start': start_time,
'Finish': end_time,
'Status': schedule.status.value
})
df = pd.DataFrame(data)
# Create Gantt chart
fig = px.timeline(
df,
x_start="Start",
x_end="Finish",
y="Task",
color="Status",
title="Content Schedule Timeline"
)
fig.update_layout(
xaxis_title="Time",
yaxis_title="Schedules",
height=max(400, len(schedules) * 30)
)
return fig
except Exception as e:
self.logger.error(f"Error creating Gantt chart: {str(e)}")
return go.Figure()
def _create_scatter_chart(self, schedules: List[Schedule]) -> go.Figure:
"""Create a scatter plot for schedules."""
try:
# Prepare data
dates = [s.scheduled_time for s in schedules]
statuses = [s.status.value for s in schedules]
ids = [s.id for s in schedules]
# Create scatter plot
fig = px.scatter(
x=dates,
y=statuses,
title="Schedule Status Over Time",
labels={'x': 'Scheduled Time', 'y': 'Status'},
hover_data={'Schedule ID': ids}
)
fig.update_layout(
xaxis_title="Scheduled Time",
yaxis_title="Status"
)
return fig
except Exception as e:
self.logger.error(f"Error creating scatter chart: {str(e)}")
return go.Figure()
def _create_bar_chart(self, schedules: List[Schedule]) -> go.Figure:
"""Create a bar chart for schedule distribution."""
try:
# Group by date
date_counts = {}
for schedule in schedules:
date_key = schedule.scheduled_time.strftime("%Y-%m-%d")
date_counts[date_key] = date_counts.get(date_key, 0) + 1
# Create bar chart
fig = px.bar(
x=list(date_counts.keys()),
y=list(date_counts.values()),
title="Scheduled Content by Date",
labels={'x': 'Date', 'y': 'Number of Schedules'}
)
fig.update_layout(
xaxis_title="Date",
yaxis_title="Number of Schedules"
)
return fig
except Exception as e:
self.logger.error(f"Error creating bar chart: {str(e)}")
return go.Figure()
def get_schedule_conflicts(
self,
schedules: List[Schedule],
time_window: int = 60 # minutes
) -> List[Dict[str, Any]]:
"""Identify potential scheduling conflicts.
Args:
schedules: List of Schedule objects
time_window: Time window in minutes to check for conflicts
Returns:
List of conflict information
"""
try:
conflicts = []
# Sort schedules by time
sorted_schedules = sorted(schedules, key=lambda x: x.scheduled_time)
for i in range(len(sorted_schedules) - 1):
current = sorted_schedules[i]
next_schedule = sorted_schedules[i + 1]
# Check if schedules are too close
time_diff = (next_schedule.scheduled_time - current.scheduled_time).total_seconds() / 60
if time_diff < time_window:
conflicts.append({
'schedule_1': current.id,
'schedule_2': next_schedule.id,
'time_1': current.scheduled_time,
'time_2': next_schedule.scheduled_time,
'gap_minutes': time_diff,
'severity': 'high' if time_diff < 30 else 'medium'
})
return conflicts
except Exception as e:
self.logger.error(f"Error finding conflicts: {str(e)}")
return []
def suggest_optimal_times(
self,
existing_schedules: List[Schedule],
target_date: datetime,
duration_hours: int = 1
) -> List[datetime]:
"""Suggest optimal times for new content based on existing schedules.
Args:
existing_schedules: List of existing Schedule objects
target_date: Target date for new content
duration_hours: Expected duration of content in hours
Returns:
List of suggested optimal times
"""
try:
suggestions = []
# Get schedules for target date
target_schedules = [
s for s in existing_schedules
if s.scheduled_time.date() == target_date.date()
]
# Define business hours (9 AM to 6 PM)
business_start = target_date.replace(hour=9, minute=0, second=0, microsecond=0)
business_end = target_date.replace(hour=18, minute=0, second=0, microsecond=0)
# Generate potential time slots (every 30 minutes)
current_time = business_start
while current_time < business_end:
# Check if this slot conflicts with existing schedules
conflict = False
for schedule in target_schedules:
schedule_end = schedule.scheduled_time + timedelta(hours=duration_hours)
slot_end = current_time + timedelta(hours=duration_hours)
# Check for overlap
if (current_time < schedule_end and slot_end > schedule.scheduled_time):
conflict = True
break
if not conflict:
suggestions.append(current_time)
current_time += timedelta(minutes=30)
return suggestions[:5] # Return top 5 suggestions
except Exception as e:
self.logger.error(f"Error suggesting optimal times: {str(e)}")
return []

View File

@@ -0,0 +1,162 @@
from typing import Dict, Any, List, Optional
from datetime import datetime, timedelta
import pytz
from .error_handling import ScheduleValidationError
def validate_schedule_data(schedule_data: Dict[str, Any]) -> None:
"""Validate schedule data before creation."""
required_fields = ['content_id', 'schedule_type', 'platforms', 'publish_date']
missing_fields = [field for field in required_fields if field not in schedule_data]
if missing_fields:
raise ScheduleValidationError(
f"Missing required fields: {', '.join(missing_fields)}",
{'missing_fields': missing_fields}
)
validate_schedule_type(schedule_data['schedule_type'])
validate_platforms(schedule_data['platforms'])
validate_publish_date(schedule_data['publish_date'])
if 'recurrence' in schedule_data:
validate_recurrence(schedule_data['recurrence'])
def validate_schedule_type(schedule_type: str) -> None:
"""Validate schedule type."""
valid_types = ['ONE_TIME', 'RECURRING', 'BATCH']
if schedule_type not in valid_types:
raise ScheduleValidationError(
f"Invalid schedule type: {schedule_type}",
{'valid_types': valid_types}
)
def validate_platforms(platforms: List[str]) -> None:
"""Validate platform list."""
valid_platforms = ['TWITTER', 'FACEBOOK', 'LINKEDIN', 'INSTAGRAM']
invalid_platforms = [p for p in platforms if p not in valid_platforms]
if invalid_platforms:
raise ScheduleValidationError(
f"Invalid platforms: {', '.join(invalid_platforms)}",
{'valid_platforms': valid_platforms}
)
if not platforms:
raise ScheduleValidationError(
"At least one platform must be specified",
{'valid_platforms': valid_platforms}
)
def validate_publish_date(publish_date: datetime) -> None:
"""Validate publish date."""
if not isinstance(publish_date, datetime):
raise ScheduleValidationError(
"Publish date must be a datetime object",
{'type': type(publish_date).__name__}
)
if publish_date.tzinfo is None:
raise ScheduleValidationError(
"Publish date must be timezone-aware",
{'date': str(publish_date)}
)
if publish_date < datetime.now(pytz.UTC):
raise ScheduleValidationError(
"Publish date must be in the future",
{'date': str(publish_date)}
)
def validate_recurrence(recurrence: Dict[str, Any]) -> None:
"""Validate recurrence settings."""
required_fields = ['frequency', 'interval']
missing_fields = [field for field in required_fields if field not in recurrence]
if missing_fields:
raise ScheduleValidationError(
f"Missing required recurrence fields: {', '.join(missing_fields)}",
{'missing_fields': missing_fields}
)
valid_frequencies = ['DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY']
if recurrence['frequency'] not in valid_frequencies:
raise ScheduleValidationError(
f"Invalid recurrence frequency: {recurrence['frequency']}",
{'valid_frequencies': valid_frequencies}
)
if not isinstance(recurrence['interval'], int) or recurrence['interval'] < 1:
raise ScheduleValidationError(
"Recurrence interval must be a positive integer",
{'interval': recurrence['interval']}
)
if 'end_date' in recurrence:
if not isinstance(recurrence['end_date'], datetime):
raise ScheduleValidationError(
"End date must be a datetime object",
{'type': type(recurrence['end_date']).__name__}
)
if recurrence['end_date'].tzinfo is None:
raise ScheduleValidationError(
"End date must be timezone-aware",
{'date': str(recurrence['end_date'])}
)
def validate_job_data(job_data: Dict[str, Any]) -> None:
"""Validate job data before creation."""
required_fields = ['content_id', 'schedule_id', 'platform']
missing_fields = [field for field in required_fields if field not in job_data]
if missing_fields:
raise ScheduleValidationError(
f"Missing required job fields: {', '.join(missing_fields)}",
{'missing_fields': missing_fields}
)
validate_platforms([job_data['platform']])
def validate_retry_settings(retry_settings: Optional[Dict[str, Any]]) -> None:
"""Validate retry settings."""
if retry_settings is None:
return
if 'max_retries' in retry_settings:
if not isinstance(retry_settings['max_retries'], int) or retry_settings['max_retries'] < 0:
raise ScheduleValidationError(
"Max retries must be a non-negative integer",
{'max_retries': retry_settings['max_retries']}
)
if 'retry_delay' in retry_settings:
if not isinstance(retry_settings['retry_delay'], (int, float)) or retry_settings['retry_delay'] < 0:
raise ScheduleValidationError(
"Retry delay must be a non-negative number",
{'retry_delay': retry_settings['retry_delay']}
)
def validate_notification_settings(notification_settings: Optional[Dict[str, Any]]) -> None:
"""Validate notification settings."""
if notification_settings is None:
return
if 'channels' in notification_settings:
valid_channels = ['EMAIL', 'SLACK', 'WEBHOOK']
invalid_channels = [c for c in notification_settings['channels'] if c not in valid_channels]
if invalid_channels:
raise ScheduleValidationError(
f"Invalid notification channels: {', '.join(invalid_channels)}",
{'valid_channels': valid_channels}
)
if 'events' in notification_settings:
valid_events = ['ON_SUCCESS', 'ON_FAILURE', 'ON_RETRY', 'ON_CANCELLATION']
invalid_events = [e for e in notification_settings['events'] if e not in valid_events]
if invalid_events:
raise ScheduleValidationError(
f"Invalid notification events: {', '.join(invalid_events)}",
{'valid_events': valid_events}
)