| Server IP : 3.147.158.171 / Your IP : 216.73.216.88 Web Server : Apache/2.4.67 (Amazon Linux) OpenSSL/3.5.5 System : Linux ip-172-31-2-178.us-east-2.compute.internal 6.1.172-216.329.amzn2023.x86_64 #1 SMP PREEMPT_DYNAMIC Wed May 20 06:31:34 UTC 2026 x86_64 User : ec2-user ( 1000) PHP Version : 8.4.21 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /tsai/www/html/api.turmansolutions.ai/ |
Upload File : |
"""
Common utilities and setup functions for cron scripts.
This module provides shared functionality for article and comment generation scripts,
reducing code duplication and ensuring consistent behavior.
"""
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path
import requests
import sys
from typing import Optional, List, Tuple
from dotenv import load_dotenv
# Load environment variables FIRST, before any app imports
env_path = Path(__file__).parent / '.env'
load_dotenv(dotenv_path=env_path)
from app.services.wp import WpService
from app.services.drupal import DrupalService
from app.services.site_config_service import SiteConfigService
from app.services.chat import ChatService
from app.appTypes import Character, SiteConfig, WpUser
def parse_article_arguments(help_text: Optional[str] = None) -> Tuple[str, Optional[str], str, bool, bool, bool, bool, bool, Optional[str], Optional[str], Optional[int], Optional[str], Optional[str], bool]:
"""
Parse command-line arguments for article creation.
Args:
help_text: Help text to display when no arguments provided (typically __doc__)
Returns:
Tuple of (site_id, character_name, character_description_override,
generate_featured_image, generate_secondary_image,
run_fact_check, research_current_events, include_previous_articles,
series_name, search_depth_override, chunks_per_source_override,
seed_image_url, seed_image_instructions, run_corrections)
"""
if len(sys.argv) < 2:
if help_text:
print(help_text)
else:
print("Usage: python create_articles.py <site_id> [options]")
sys.exit(1)
site_id = sys.argv[1]
# Parse --character <name>
character_name = None
if '--character' in sys.argv:
idx = sys.argv.index('--character')
if idx + 1 < len(sys.argv):
character_name = sys.argv[idx + 1]
else:
print("Error: --character requires a character name")
sys.exit(1)
# Parse --character-description-file <path>
character_description_override = ''
if '--character-description-file' in sys.argv:
idx = sys.argv.index('--character-description-file')
if idx + 1 < len(sys.argv):
description_file = sys.argv[idx + 1]
try:
with open(description_file, 'r', encoding='utf-8') as f:
character_description_override = f.read().strip()
print(f"Loaded character description override from: {description_file}")
except FileNotFoundError:
print(f"Error: Character description file not found: {description_file}")
sys.exit(1)
except Exception as e:
print(f"Error reading character description file: {e}")
sys.exit(1)
else:
print("Error: --character-description-file requires a file path")
sys.exit(1)
# Parse article-specific boolean flags
generate_featured_image = '--featured-image' in sys.argv
generate_secondary_image = '--secondary-image' in sys.argv
run_fact_check = '--fact-check' in sys.argv
run_corrections = '--corrections' in sys.argv
research_current_events = '--current-events' in sys.argv
include_previous_articles = '--no-previous-articles' not in sys.argv
# Parse --series <name>
series_name = None
if '--series' in sys.argv:
idx = sys.argv.index('--series')
if idx + 1 < len(sys.argv):
series_name = sys.argv[idx + 1]
else:
print("Error: --series requires a series name")
sys.exit(1)
# Parse --search-depth <value>
search_depth_override = None
if '--search-depth' in sys.argv:
idx = sys.argv.index('--search-depth')
if idx + 1 < len(sys.argv):
search_depth_override = sys.argv[idx + 1]
if search_depth_override not in ('basic', 'advanced'):
print("Error: --search-depth must be 'basic' or 'advanced'")
sys.exit(1)
else:
print("Error: --search-depth requires a value (basic or advanced)")
sys.exit(1)
# Parse --chunks-per-source <value>
chunks_per_source_override = None
if '--chunks-per-source' in sys.argv:
idx = sys.argv.index('--chunks-per-source')
if idx + 1 < len(sys.argv):
try:
chunks_per_source_override = int(sys.argv[idx + 1])
if chunks_per_source_override < 1 or chunks_per_source_override > 3:
print("Error: --chunks-per-source must be between 1 and 3")
sys.exit(1)
except ValueError:
print("Error: --chunks-per-source requires an integer value")
sys.exit(1)
else:
print("Error: --chunks-per-source requires a value (1-3)")
sys.exit(1)
# Parse --seed-image-url <url>
seed_image_url = None
if '--seed-image-url' in sys.argv:
idx = sys.argv.index('--seed-image-url')
if idx + 1 < len(sys.argv):
seed_image_url = sys.argv[idx + 1]
else:
print("Error: --seed-image-url requires a URL")
sys.exit(1)
# Parse --seed-image-instructions <text> or --seed-image-instructions-file <path>
seed_image_instructions = None
if '--seed-image-instructions' in sys.argv:
idx = sys.argv.index('--seed-image-instructions')
if idx + 1 < len(sys.argv):
seed_image_instructions = sys.argv[idx + 1]
else:
print("Error: --seed-image-instructions requires a string")
sys.exit(1)
if '--seed-image-instructions-file' in sys.argv:
idx = sys.argv.index('--seed-image-instructions-file')
if idx + 1 < len(sys.argv):
seed_instructions_file = sys.argv[idx + 1]
try:
with open(seed_instructions_file, 'r', encoding='utf-8') as f:
seed_image_instructions = f.read().strip()
print(f"Loaded seed image instructions from: {seed_instructions_file}")
except FileNotFoundError:
print(f"Error: Seed image instructions file not found: {seed_instructions_file}")
sys.exit(1)
except Exception as e:
print(f"Error reading seed image instructions file: {e}")
sys.exit(1)
else:
print("Error: --seed-image-instructions-file requires a file path")
sys.exit(1)
return (site_id, character_name, character_description_override,
generate_featured_image, generate_secondary_image,
run_fact_check, research_current_events, include_previous_articles,
series_name, search_depth_override, chunks_per_source_override,
seed_image_url, seed_image_instructions, run_corrections)
def parse_comment_arguments(help_text: Optional[str] = None) -> Tuple[str, Optional[str], str]:
"""
Parse command-line arguments for comment creation.
Args:
help_text: Help text to display when no arguments provided (typically __doc__)
Returns:
Tuple of (site_id, character_name, user_instructions)
"""
if len(sys.argv) < 2:
if help_text:
print(help_text)
else:
print("Usage: python create_comments.py <site_id> [options]")
sys.exit(1)
site_id = sys.argv[1]
# Parse --character <name>
character_name = None
if '--character' in sys.argv:
idx = sys.argv.index('--character')
if idx + 1 < len(sys.argv):
character_name = sys.argv[idx + 1]
else:
print("Error: --character requires a character name")
sys.exit(1)
# Parse --instructions-file <path>
user_instructions = ''
if '--instructions-file' in sys.argv:
idx = sys.argv.index('--instructions-file')
if idx + 1 < len(sys.argv):
instructions_file = sys.argv[idx + 1]
try:
with open(instructions_file, 'r', encoding='utf-8') as f:
user_instructions = f.read().strip()
print(f"Loaded instructions from: {instructions_file}")
except FileNotFoundError:
print(f"Error: Instructions file not found: {instructions_file}")
sys.exit(1)
except Exception as e:
print(f"Error reading instructions file: {e}")
sys.exit(1)
else:
print("Error: --instructions-file requires a file path")
sys.exit(1)
return site_id, character_name, user_instructions
def check_internet_connectivity() -> None:
"""
Internet connectivity check — disabled.
On production servers, connectivity is guaranteed.
"""
pass
def setup_logging(log_filename: str) -> None:
"""
Configure logging with rotating file handler.
Args:
log_filename: Name of the log file (e.g., 'api.log')
"""
log_path = f'logs/{log_filename}'
handler = RotatingFileHandler(log_path, maxBytes=10*1024*1024, backupCount=5)
logging.basicConfig(
level=logging.INFO,
format='%(levelname)s %(asctime)s %(filename)s ln %(lineno)s %(message)s',
datefmt='%m/%d/%Y %I:%M:%S %p',
handlers=[handler]
)
def initialize_services() -> Tuple[WpService, DrupalService, SiteConfigService, ChatService]:
"""
Initialize all required service instances.
Returns:
Tuple of (wp_service, drupal_service, site_config_service, chat_service)
"""
wp_service = WpService()
drupal_service = DrupalService()
site_config_service = SiteConfigService()
chat_service = ChatService()
return wp_service, drupal_service, site_config_service, chat_service
def load_site_config(site_config_service: SiteConfigService, site_id: str) -> SiteConfig:
"""
Load and validate site configuration.
Args:
site_config_service: Site configuration service instance
site_id: Site slug identifier
Returns:
Site configuration object
"""
site = site_config_service.get_site_by_slug(site_id)
print(f'site config is {site}')
if not site:
logging.error(f"Invalid site slug: {site_id}")
print(f"Invalid slug!")
sys.exit(1)
logging.info(f'site config is {site}')
return site
def get_jwt_token(wp_service: WpService, site: SiteConfig, site_id: str) -> str:
"""
Retrieve WordPress JWT token for the site.
Args:
wp_service: WordPress service instance
site: Site configuration object
site_id: Site slug identifier
Returns:
JWT token string
"""
wp_token = wp_service.get_jwt_token(site)
if not wp_token or not isinstance(wp_token, str):
logging.error(f"Failed to get JWT token for site {site_id}: {wp_token}")
print(f"Invalid token!")
sys.exit(1)
return wp_token
def fetch_and_filter_characters(
drupal_service: DrupalService,
wp_service: WpService,
site: SiteConfig,
site_id: str,
wp_token: str
) -> List[Character]:
"""
Fetch characters from Drupal, filter by site, and add WordPress user data.
Args:
drupal_service: Drupal service instance
wp_service: WordPress service instance
site: Site configuration object
site_id: Site slug identifier
wp_token: WordPress JWT token
Returns:
List of filtered characters with WordPress user data
"""
try:
# Fetch only characters owned by this site's Drupal user
if not site.drupal_target_id:
logging.error(f"No drupal_target_id configured for site {site_id}")
print(f"Error: No drupal_target_id configured for site {site_id}")
sys.exit(1)
character_data = drupal_service.fetch_characters_by_uid(site.drupal_target_id)
system_wide_data = drupal_service.fetch_system_wide_characters()
character_data = character_data + system_wide_data
logging.info(f'fetched characters for target_id {site.drupal_target_id} (incl. system-wide): {len(character_data)}')
characters = drupal_service.create_characters(site, character_data)
wp_users_data = wp_service.get_users(site, wp_token)
# Ensure wp_users_data is not None
if wp_users_data is None:
wp_users_data = []
logging.warning("No WordPress users returned, using empty list")
filtered_characters = drupal_service.filter_characters(
characters, site_id, wp_users_data
)
logging.info(f'filtered characters: {len(filtered_characters)}')
except Exception as e:
logging.error(f"Failed to fetch or filter characters: {e}", exc_info=True)
print(f"Error fetching characters: {e}")
sys.exit(1)
# Add WordPress user data to each character
for character in filtered_characters:
wp_service.add_to_character(site, character, wp_users_data)
return filtered_characters
def select_characters(
filtered_characters: List[Character],
drupal_service: DrupalService,
site: SiteConfig,
character_name: Optional[str] = None,
field: str = 'commentsPerDay'
) -> List[Character]:
"""
Select characters for content generation.
If character_name is provided, only that character is selected.
Otherwise, uses the calendar-schedule selection algorithm.
Args:
filtered_characters: List of all available characters
drupal_service: Drupal service instance
site: Site configuration object
character_name: Optional specific character name to use
field: Content type to schedule against ('commentsPerDay' or 'articlesPerDay'),
used to pick the matching calendar schedule
Returns:
List of selected characters
"""
if character_name:
# Single character mode
selected_character = None
for character in filtered_characters:
if character.name == character_name:
selected_character = character
break
if selected_character:
characters = [selected_character]
print(f'SINGLE CHARACTER MODE: Using {character_name}')
logging.info(f'Single character mode: {character_name}')
else:
print(f'Error: Character "{character_name}" not found in filtered characters')
logging.error(f'Character "{character_name}" not found')
available = [c.name for c in filtered_characters]
print(f'Available characters: {", ".join(available)}')
sys.exit(1)
else:
# Normal mode: use calendar-schedule selection
characters = drupal_service.selectCharacters(filtered_characters, field=field)
if not characters:
logging.info('No characters selected.')
print('No characters selected.')
sys.exit(0)
return characters
def load_character_articles(
characters: List[Character],
wp_service: WpService,
site: SiteConfig,
wp_token: str
) -> None:
"""
Load each character's own articles (filtered by author).
Modifies characters in-place by setting their articles attribute.
Args:
characters: List of characters to load articles for
wp_service: WordPress service instance
site: Site configuration object
wp_token: WordPress JWT token
"""
for character in characters:
logging.info(f'Name: {character.name} IS POSTING!')
logging.info(f'Description: {character.description}')
# Load articles filtered by author using WordPress API
if character.wp_user and isinstance(character.wp_user, WpUser):
character.articles = wp_service.get_recent_articles(
site, wp_token, 100, author_id=character.wp_user.id
)
if character.articles is None:
character.articles = []
logging.warning(f"No articles returned for author {character.wp_user.id}")
else:
character.articles = []
logging.info(f"Character articles: {len(character.articles)}")
def filter_already_posted_today(
characters: List[Character],
wp_service: WpService,
site: SiteConfig,
wp_token: str
) -> List[Character]:
"""
Filter out characters that have already posted an article today.
Args:
characters: List of selected characters
wp_service: WordPress service instance
site: Site configuration object
wp_token: WordPress JWT token
Returns:
List of characters that have not yet posted today
"""
filtered = []
for character in characters:
if character.wp_user and isinstance(character.wp_user, WpUser):
if wp_service.has_author_posted_today(site, wp_token, character.wp_user.id):
logging.info(f"Skipping {character.name}: already posted an article today")
print(f"Skipping {character.name}: already posted an article today")
continue
filtered.append(character)
if len(filtered) < len(characters):
skipped = len(characters) - len(filtered)
logging.info(f"Filtered out {skipped} character(s) that already posted today")
return filtered
def filter_already_commented_today(
characters: List[Character],
wp_service: WpService,
site: SiteConfig,
wp_token: str
) -> List[Character]:
"""
Filter out characters that have already posted a comment today.
Args:
characters: List of selected characters
wp_service: WordPress service instance
site: Site configuration object
wp_token: WordPress JWT token
Returns:
List of characters that have not yet commented today
"""
filtered = []
for character in characters:
if character.wp_user and isinstance(character.wp_user, WpUser):
if wp_service.has_author_commented_today(site, wp_token, character.wp_user.id):
logging.info(f"Skipping {character.name}: already commented today")
print(f"Skipping {character.name}: already commented today")
continue
filtered.append(character)
if len(filtered) < len(characters):
skipped = len(characters) - len(filtered)
logging.info(f"Filtered out {skipped} character(s) that already commented today")
return filtered