This commit is contained in:
alorig
2025-11-18 05:21:27 +05:00
parent a0f3e3a778
commit 9a6d47b91b
34 changed files with 3258 additions and 9 deletions

View File

@@ -0,0 +1,99 @@
"""
Base Adapter
Phase 6: Site Integration & Multi-Destination Publishing
Abstract base class for publishing adapters.
"""
from abc import ABC, abstractmethod
from typing import Dict, Any, Optional
class BaseAdapter(ABC):
"""
Abstract base class for publishing adapters.
All platform-specific adapters must inherit from this.
"""
@abstractmethod
def publish(
self,
content: Any,
destination_config: Dict[str, Any]
) -> Dict[str, Any]:
"""
Publish content to destination.
Args:
content: Content or SiteBlueprint to publish
destination_config: Destination-specific configuration
Returns:
dict: {
'success': bool,
'external_id': str, # External platform ID
'url': str, # Published content URL
'published_at': datetime,
'metadata': dict
}
"""
pass
@abstractmethod
def test_connection(
self,
config: Dict[str, Any]
) -> Dict[str, Any]:
"""
Test connection to destination.
Args:
config: Destination configuration
Returns:
dict: {
'success': bool,
'message': str,
'details': dict
}
"""
pass
@abstractmethod
def get_status(
self,
published_id: str,
config: Dict[str, Any]
) -> Dict[str, Any]:
"""
Get publishing status for published content.
Args:
published_id: External platform ID
config: Destination configuration
Returns:
dict: {
'status': str, # 'published', 'draft', 'deleted', etc.
'url': str,
'updated_at': datetime,
'metadata': dict
}
"""
pass
def validate_config(
self,
config: Dict[str, Any]
) -> tuple:
"""
Validate destination configuration.
Args:
config: Destination configuration
Returns:
tuple: (is_valid, error_message)
"""
# Default implementation - can be overridden
return True, None

View File

@@ -0,0 +1,147 @@
"""
Shopify Adapter
Phase 6: Site Integration & Multi-Destination Publishing
Adapter for publishing content to Shopify.
Skeleton implementation - to be fully implemented in future.
"""
import logging
from typing import Dict, Any
from datetime import datetime
from igny8_core.business.publishing.services.adapters.base_adapter import BaseAdapter
logger = logging.getLogger(__name__)
class ShopifyAdapter(BaseAdapter):
"""
Adapter for publishing content to Shopify.
Skeleton implementation - full implementation pending.
"""
def publish(
self,
content: Any,
destination_config: Dict[str, Any]
) -> Dict[str, Any]:
"""
Publish content to Shopify.
Args:
content: Content instance or dict with content data
destination_config: {
'shop_domain': str,
'access_token': str,
'content_type': str, # 'page', 'blog_post', 'product'
}
Returns:
dict: {
'success': bool,
'external_id': str, # Shopify resource ID
'url': str, # Published resource URL
'published_at': datetime,
'metadata': dict
}
"""
# TODO: Implement Shopify publishing
logger.warning("[ShopifyAdapter] Shopify publishing not yet implemented")
return {
'success': False,
'external_id': None,
'url': None,
'published_at': None,
'metadata': {
'error': 'Shopify publishing not yet implemented'
}
}
def test_connection(
self,
config: Dict[str, Any]
) -> Dict[str, Any]:
"""
Test connection to Shopify.
Args:
config: {
'shop_domain': str,
'access_token': str
}
Returns:
dict: {
'success': bool,
'message': str,
'details': dict
}
"""
# TODO: Implement Shopify connection testing
logger.warning("[ShopifyAdapter] Shopify connection testing not yet implemented")
return {
'success': False,
'message': 'Shopify connection testing not yet implemented',
'details': {}
}
def get_status(
self,
published_id: str,
config: Dict[str, Any]
) -> Dict[str, Any]:
"""
Get publishing status for published content.
Args:
published_id: Shopify resource ID
config: Shopify configuration
Returns:
dict: {
'status': str, # 'published', 'draft', 'deleted', etc.
'url': str,
'updated_at': datetime,
'metadata': dict
}
"""
# TODO: Implement Shopify status retrieval
logger.warning("[ShopifyAdapter] Shopify status retrieval not yet implemented")
return {
'status': 'unknown',
'url': None,
'updated_at': None,
'metadata': {
'error': 'Shopify status retrieval not yet implemented'
}
}
def validate_config(
self,
config: Dict[str, Any]
) -> tuple:
"""
Validate Shopify configuration.
Args:
config: Shopify configuration
Returns:
tuple: (is_valid, error_message)
"""
required_fields = ['shop_domain', 'access_token']
for field in required_fields:
if field not in config or not config[field]:
return False, f"Missing required field: {field}"
# Validate domain format
shop_domain = config.get('shop_domain', '')
if not shop_domain.endswith('.myshopify.com'):
return False, "shop_domain must end with .myshopify.com"
return True, None

View File

@@ -0,0 +1,265 @@
"""
WordPress Adapter
Phase 6: Site Integration & Multi-Destination Publishing
Adapter for publishing content to WordPress.
Refactored to use BaseAdapter interface while preserving existing functionality.
"""
import logging
from typing import Dict, Any, Optional
from datetime import datetime
from igny8_core.business.publishing.services.adapters.base_adapter import BaseAdapter
from igny8_core.utils.wordpress import WordPressClient
logger = logging.getLogger(__name__)
class WordPressAdapter(BaseAdapter):
"""
Adapter for publishing content to WordPress.
Uses WordPressClient internally to preserve existing functionality.
"""
def publish(
self,
content: Any,
destination_config: Dict[str, Any]
) -> Dict[str, Any]:
"""
Publish content to WordPress.
Args:
content: Content instance or dict with content data
destination_config: {
'site_url': str,
'username': str,
'app_password': str,
'status': str (optional, default 'draft'),
'featured_image_url': str (optional)
}
Returns:
dict: {
'success': bool,
'external_id': str, # WordPress post ID
'url': str, # Published post URL
'published_at': datetime,
'metadata': dict
}
"""
try:
# Get WordPress client
client = self._get_client(destination_config)
# Extract content data
if hasattr(content, 'title') and hasattr(content, 'content'):
# Content model instance
title = content.title
content_html = content.content
elif isinstance(content, dict):
# Dict with content data
title = content.get('title', '')
content_html = content.get('content', '')
else:
raise ValueError(f"Unsupported content type: {type(content)}")
# Get publishing options
status = destination_config.get('status', 'draft')
featured_image_url = destination_config.get('featured_image_url')
# Publish to WordPress
result = client.create_post(
title=title,
content=content_html,
status=status,
featured_image_url=featured_image_url
)
if result.get('success'):
return {
'success': True,
'external_id': str(result.get('post_id')),
'url': result.get('url'),
'published_at': datetime.now(),
'metadata': {
'post_id': result.get('post_id'),
'status': status
}
}
else:
return {
'success': False,
'external_id': None,
'url': None,
'published_at': None,
'metadata': {
'error': result.get('error')
}
}
except Exception as e:
logger.error(
f"[WordPressAdapter] Error publishing content: {str(e)}",
exc_info=True
)
return {
'success': False,
'external_id': None,
'url': None,
'published_at': None,
'metadata': {
'error': str(e)
}
}
def test_connection(
self,
config: Dict[str, Any]
) -> Dict[str, Any]:
"""
Test connection to WordPress.
Args:
config: {
'site_url': str,
'username': str,
'app_password': str
}
Returns:
dict: {
'success': bool,
'message': str,
'details': dict
}
"""
try:
client = self._get_client(config)
result = client.test_connection()
return {
'success': result.get('success', False),
'message': result.get('message', ''),
'details': {
'wp_version': result.get('wp_version')
}
}
except Exception as e:
logger.error(
f"[WordPressAdapter] Connection test failed: {str(e)}",
exc_info=True
)
return {
'success': False,
'message': str(e),
'details': {}
}
def get_status(
self,
published_id: str,
config: Dict[str, Any]
) -> Dict[str, Any]:
"""
Get publishing status for published content.
Args:
published_id: WordPress post ID
config: WordPress configuration
Returns:
dict: {
'status': str, # 'published', 'draft', 'deleted', etc.
'url': str,
'updated_at': datetime,
'metadata': dict
}
"""
try:
client = self._get_client(config)
# Fetch post from WordPress
response = client.session.get(
f"{client.api_base}/posts/{published_id}"
)
if response.status_code == 200:
data = response.json()
return {
'status': data.get('status', 'unknown'),
'url': data.get('link'),
'updated_at': datetime.fromisoformat(
data.get('modified', '').replace('Z', '+00:00')
) if data.get('modified') else None,
'metadata': {
'title': data.get('title', {}).get('rendered'),
'author': data.get('author'),
}
}
else:
return {
'status': 'deleted' if response.status_code == 404 else 'unknown',
'url': None,
'updated_at': None,
'metadata': {
'error': f"HTTP {response.status_code}"
}
}
except Exception as e:
logger.error(
f"[WordPressAdapter] Error getting status: {str(e)}",
exc_info=True
)
return {
'status': 'unknown',
'url': None,
'updated_at': None,
'metadata': {
'error': str(e)
}
}
def validate_config(
self,
config: Dict[str, Any]
) -> tuple:
"""
Validate WordPress configuration.
Args:
config: WordPress configuration
Returns:
tuple: (is_valid, error_message)
"""
required_fields = ['site_url']
for field in required_fields:
if field not in config or not config[field]:
return False, f"Missing required field: {field}"
# Validate URL format
site_url = config.get('site_url', '')
if not site_url.startswith(('http://', 'https://')):
return False, "site_url must start with http:// or https://"
return True, None
def _get_client(self, config: Dict[str, Any]) -> WordPressClient:
"""
Get WordPress client from configuration.
Args:
config: WordPress configuration
Returns:
WordPressClient instance
"""
site_url = config.get('site_url')
username = config.get('username')
app_password = config.get('app_password')
return WordPressClient(site_url, username, app_password)

View File

@@ -138,8 +138,24 @@ class PublisherService:
if not adapter:
raise ValueError(f"No adapter found for destination: {destination}")
# Get destination config (for now, basic config - can be extended)
destination_config = {'account': account}
# If content has site, try to get integration config
if hasattr(content, 'site') and content.site:
from igny8_core.business.integration.models import SiteIntegration
integration = SiteIntegration.objects.filter(
site=content.site,
platform=destination,
is_active=True
).first()
if integration:
destination_config.update(integration.config_json)
destination_config.update(integration.get_credentials())
# Publish via adapter
result = adapter.publish(content, {'account': account})
result = adapter.publish(content, destination_config)
# Update record
record.status = 'published' if result.get('success') else 'failed'
@@ -163,6 +179,156 @@ class PublisherService:
record.save()
raise
def publish_to_multiple_destinations(
self,
content: Any,
destinations: List[Dict[str, Any]],
account
) -> Dict[str, Any]:
"""
Publish content to multiple destinations.
Args:
content: Content instance or SiteBlueprint
destinations: List of destination configs, e.g.:
[
{'platform': 'wordpress', 'site_url': '...', 'username': '...', 'app_password': '...'},
{'platform': 'sites'},
{'platform': 'shopify', 'shop_domain': '...', 'access_token': '...'}
]
account: Account instance
Returns:
dict: {
'success': bool,
'results': list of publishing results per destination
}
"""
results = []
for destination_config in destinations:
platform = destination_config.get('platform')
if not platform:
results.append({
'platform': 'unknown',
'success': False,
'error': 'Platform not specified'
})
continue
try:
adapter = self._get_adapter(platform)
if not adapter:
results.append({
'platform': platform,
'success': False,
'error': f'No adapter found for platform: {platform}'
})
continue
# Validate config
is_valid, error_msg = adapter.validate_config(destination_config)
if not is_valid:
results.append({
'platform': platform,
'success': False,
'error': error_msg or 'Invalid configuration'
})
continue
# Publish via adapter
result = adapter.publish(content, destination_config)
# Create publishing record if content has site/sector
if hasattr(content, 'site') and hasattr(content, 'sector'):
record = PublishingRecord.objects.create(
account=account,
site=content.site,
sector=content.sector,
content=content if hasattr(content, 'id') and not isinstance(content, SiteBlueprint) else None,
site_blueprint=content if isinstance(content, SiteBlueprint) else None,
destination=platform,
status='published' if result.get('success') else 'failed',
destination_id=result.get('external_id'),
destination_url=result.get('url'),
published_at=result.get('published_at'),
error_message=result.get('metadata', {}).get('error'),
metadata=result.get('metadata', {})
)
result['publishing_record_id'] = record.id
results.append({
'platform': platform,
'success': result.get('success', False),
'external_id': result.get('external_id'),
'url': result.get('url'),
'error': result.get('metadata', {}).get('error')
})
except Exception as e:
logger.error(
f"Error publishing to {platform}: {str(e)}",
exc_info=True
)
results.append({
'platform': platform,
'success': False,
'error': str(e)
})
return {
'success': all(r.get('success', False) for r in results),
'results': results
}
def publish_with_integrations(
self,
content: Any,
site,
account,
platforms: Optional[List[str]] = None
) -> Dict[str, Any]:
"""
Publish content using site integrations.
Args:
content: Content instance or SiteBlueprint
site: Site instance
account: Account instance
platforms: Optional list of platforms to publish to (all active if None)
Returns:
dict: Publishing results
"""
from igny8_core.business.integration.models import SiteIntegration
# Get active integrations for site
integrations = SiteIntegration.objects.filter(
site=site,
is_active=True
)
if platforms:
integrations = integrations.filter(platform__in=platforms)
destinations = []
for integration in integrations:
config = integration.config_json.copy()
credentials = integration.get_credentials()
destination_config = {
'platform': integration.platform,
**config,
**credentials
}
destinations.append(destination_config)
# Also add 'sites' destination if not in platforms filter or if platforms is None
if not platforms or 'sites' in platforms:
destinations.append({'platform': 'sites'})
return self.publish_to_multiple_destinations(content, destinations, account)
def _get_adapter(self, destination: str):
"""
Get adapter for destination platform.
@@ -178,10 +344,10 @@ class PublisherService:
from igny8_core.business.publishing.services.adapters.sites_renderer_adapter import SitesRendererAdapter
return SitesRendererAdapter()
elif destination == 'wordpress':
# Will be implemented in Phase 6
return None
from igny8_core.business.publishing.services.adapters.wordpress_adapter import WordPressAdapter
return WordPressAdapter()
elif destination == 'shopify':
# Will be implemented in Phase 6
return None
from igny8_core.business.publishing.services.adapters.shopify_adapter import ShopifyAdapter
return ShopifyAdapter()
return None