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,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}
)