This commit is contained in:
alorig
2025-11-18 05:03:27 +05:00
parent 342d9eab17
commit 40d379dd7e
35 changed files with 2073 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
"""
Publishing Services
"""

View File

@@ -0,0 +1,4 @@
"""
Publishing Adapters
"""

View File

@@ -0,0 +1,203 @@
"""
Sites Renderer Adapter
Phase 5: Sites Renderer & Publishing
Adapter for deploying sites to IGNY8 Sites renderer.
"""
import logging
import json
import os
from typing import Dict, Any
from pathlib import Path
from igny8_core.business.site_building.models import SiteBlueprint
from igny8_core.business.publishing.models import DeploymentRecord
logger = logging.getLogger(__name__)
class SitesRendererAdapter:
"""
Adapter for deploying sites to IGNY8 Sites renderer.
Writes site definitions to filesystem for Sites container to serve.
"""
def __init__(self):
self.sites_data_path = os.getenv('SITES_DATA_PATH', '/data/app/sites-data')
def deploy(self, site_blueprint: SiteBlueprint) -> Dict[str, Any]:
"""
Deploy site blueprint to Sites renderer.
Args:
site_blueprint: SiteBlueprint instance to deploy
Returns:
dict: Deployment result with status and deployment record
"""
try:
# Create deployment record
deployment = DeploymentRecord.objects.create(
account=site_blueprint.account,
site=site_blueprint.site,
sector=site_blueprint.sector,
site_blueprint=site_blueprint,
version=site_blueprint.version,
status='deploying'
)
# Build site definition
site_definition = self._build_site_definition(site_blueprint)
# Write to filesystem
deployment_path = self._write_site_definition(
site_blueprint,
site_definition,
deployment.version
)
# Update deployment record
deployment.status = 'deployed'
deployment.deployed_version = site_blueprint.version
deployment.deployment_url = self._get_deployment_url(site_blueprint)
deployment.metadata = {
'deployment_path': str(deployment_path),
'site_definition': site_definition
}
deployment.save()
# Update blueprint
site_blueprint.deployed_version = site_blueprint.version
site_blueprint.status = 'deployed'
site_blueprint.save(update_fields=['deployed_version', 'status', 'updated_at'])
logger.info(
f"[SitesRendererAdapter] Successfully deployed site {site_blueprint.id} v{deployment.version}"
)
return {
'success': True,
'deployment_id': deployment.id,
'version': deployment.version,
'deployment_url': deployment.deployment_url,
'deployment_path': str(deployment_path)
}
except Exception as e:
logger.error(
f"[SitesRendererAdapter] Error deploying site {site_blueprint.id}: {str(e)}",
exc_info=True
)
# Update deployment record with error
if 'deployment' in locals():
deployment.status = 'failed'
deployment.error_message = str(e)
deployment.save()
return {
'success': False,
'error': str(e)
}
def _build_site_definition(self, site_blueprint: SiteBlueprint) -> Dict[str, Any]:
"""
Build site definition JSON from blueprint.
Args:
site_blueprint: SiteBlueprint instance
Returns:
dict: Site definition structure
"""
# Get all pages
pages = []
for page in site_blueprint.pages.all().order_by('order'):
pages.append({
'id': page.id,
'slug': page.slug,
'title': page.title,
'type': page.type,
'blocks': page.blocks_json,
'status': page.status,
})
# Build site definition
definition = {
'id': site_blueprint.id,
'name': site_blueprint.name,
'description': site_blueprint.description,
'version': site_blueprint.version,
'layout': site_blueprint.structure_json.get('layout', 'default'),
'theme': site_blueprint.structure_json.get('theme', {}),
'navigation': site_blueprint.structure_json.get('navigation', []),
'pages': pages,
'config': site_blueprint.config_json,
'created_at': site_blueprint.created_at.isoformat(),
'updated_at': site_blueprint.updated_at.isoformat(),
}
return definition
def _write_site_definition(
self,
site_blueprint: SiteBlueprint,
site_definition: Dict[str, Any],
version: int
) -> Path:
"""
Write site definition to filesystem.
Args:
site_blueprint: SiteBlueprint instance
site_definition: Site definition dict
version: Version number
Returns:
Path: Deployment path
"""
# Build path: /data/app/sites-data/clients/{site_id}/v{version}/
site_id = site_blueprint.site.id
deployment_dir = Path(self.sites_data_path) / 'clients' / str(site_id) / f'v{version}'
deployment_dir.mkdir(parents=True, exist_ok=True)
# Write site.json
site_json_path = deployment_dir / 'site.json'
with open(site_json_path, 'w', encoding='utf-8') as f:
json.dump(site_definition, f, indent=2, ensure_ascii=False)
# Write pages
pages_dir = deployment_dir / 'pages'
pages_dir.mkdir(exist_ok=True)
for page in site_definition.get('pages', []):
page_json_path = pages_dir / f"{page['slug']}.json"
with open(page_json_path, 'w', encoding='utf-8') as f:
json.dump(page, f, indent=2, ensure_ascii=False)
# Ensure assets directory exists
assets_dir = deployment_dir / 'assets'
assets_dir.mkdir(exist_ok=True)
(assets_dir / 'images').mkdir(exist_ok=True)
(assets_dir / 'documents').mkdir(exist_ok=True)
(assets_dir / 'media').mkdir(exist_ok=True)
logger.info(f"[SitesRendererAdapter] Wrote site definition to {deployment_dir}")
return deployment_dir
def _get_deployment_url(self, site_blueprint: SiteBlueprint) -> str:
"""
Get deployment URL for site.
Args:
site_blueprint: SiteBlueprint instance
Returns:
str: Deployment URL
"""
# TODO: Implement URL generation based on site configuration
# For now, return placeholder
site_id = site_blueprint.site.id
return f"https://{site_id}.igny8.com" # Placeholder

View File

@@ -0,0 +1,140 @@
"""
Deployment Service
Phase 5: Sites Renderer & Publishing
Manages deployment lifecycle for sites.
"""
import logging
from typing import Optional
from igny8_core.business.site_building.models import SiteBlueprint
from igny8_core.business.publishing.models import DeploymentRecord
logger = logging.getLogger(__name__)
class DeploymentService:
"""
Service for managing site deployment lifecycle.
"""
def get_status(self, site_blueprint: SiteBlueprint) -> Optional[DeploymentRecord]:
"""
Get current deployment status for a site.
Args:
site_blueprint: SiteBlueprint instance
Returns:
DeploymentRecord or None
"""
return DeploymentRecord.objects.filter(
site_blueprint=site_blueprint,
status='deployed'
).order_by('-deployed_at').first()
def get_latest_deployment(
self,
site_blueprint: SiteBlueprint
) -> Optional[DeploymentRecord]:
"""
Get latest deployment record (any status).
Args:
site_blueprint: SiteBlueprint instance
Returns:
DeploymentRecord or None
"""
return DeploymentRecord.objects.filter(
site_blueprint=site_blueprint
).order_by('-created_at').first()
def rollback(
self,
site_blueprint: SiteBlueprint,
target_version: int
) -> dict:
"""
Rollback site to a previous version.
Args:
site_blueprint: SiteBlueprint instance
target_version: Version to rollback to
Returns:
dict: Rollback result
"""
try:
# Find deployment record for target version
target_deployment = DeploymentRecord.objects.filter(
site_blueprint=site_blueprint,
version=target_version,
status='deployed'
).first()
if not target_deployment:
return {
'success': False,
'error': f'Deployment for version {target_version} not found'
}
# Create new deployment record for rollback
rollback_deployment = DeploymentRecord.objects.create(
account=site_blueprint.account,
site=site_blueprint.site,
sector=site_blueprint.sector,
site_blueprint=site_blueprint,
version=target_version,
status='deployed',
deployed_version=target_version,
deployment_url=target_deployment.deployment_url,
metadata={
'rollback_from': site_blueprint.version,
'rollback_to': target_version
}
)
# Update blueprint
site_blueprint.deployed_version = target_version
site_blueprint.save(update_fields=['deployed_version', 'updated_at'])
logger.info(
f"[DeploymentService] Rolled back site {site_blueprint.id} to version {target_version}"
)
return {
'success': True,
'deployment_id': rollback_deployment.id,
'version': target_version
}
except Exception as e:
logger.error(
f"[DeploymentService] Error rolling back site {site_blueprint.id}: {str(e)}",
exc_info=True
)
return {
'success': False,
'error': str(e)
}
def list_deployments(
self,
site_blueprint: SiteBlueprint
) -> list:
"""
List all deployments for a site.
Args:
site_blueprint: SiteBlueprint instance
Returns:
list: List of DeploymentRecord instances
"""
return list(
DeploymentRecord.objects.filter(
site_blueprint=site_blueprint
).order_by('-created_at')
)

View File

@@ -0,0 +1,187 @@
"""
Publisher Service
Phase 5: Sites Renderer & Publishing
Main publishing orchestrator for content and sites.
"""
import logging
from typing import Optional, List, Dict, Any
from igny8_core.business.publishing.models import PublishingRecord, DeploymentRecord
from igny8_core.business.site_building.models import SiteBlueprint
logger = logging.getLogger(__name__)
class PublisherService:
"""
Main publishing service for content and sites.
Routes to appropriate adapters based on destination.
"""
def __init__(self):
self.adapters = {}
self._load_adapters()
def _load_adapters(self):
"""Lazy load adapters to avoid circular imports"""
pass # Will be implemented when adapters are created
def publish_to_sites(self, site_blueprint: SiteBlueprint) -> Dict[str, Any]:
"""
Publish site to Sites renderer.
Args:
site_blueprint: SiteBlueprint instance to deploy
Returns:
dict: Deployment result with status and deployment record
"""
from igny8_core.business.publishing.services.adapters.sites_renderer_adapter import SitesRendererAdapter
adapter = SitesRendererAdapter()
return adapter.deploy(site_blueprint)
def get_deployment_status(self, site_blueprint: SiteBlueprint) -> Optional[DeploymentRecord]:
"""
Get deployment status for a site.
Args:
site_blueprint: SiteBlueprint instance
Returns:
DeploymentRecord or None
"""
from igny8_core.business.publishing.services.deployment_service import DeploymentService
service = DeploymentService()
return service.get_status(site_blueprint)
def publish_content(
self,
content_id: int,
destinations: List[str],
account
) -> Dict[str, Any]:
"""
Publish content to multiple destinations.
Args:
content_id: Content ID to publish
destinations: List of destination platforms ('wordpress', 'sites', 'shopify')
account: Account instance
Returns:
dict: Publishing results per destination
"""
from igny8_core.business.content.models import Content
try:
content = Content.objects.get(id=content_id, account=account)
except Content.DoesNotExist:
return {
'success': False,
'error': f'Content {content_id} not found'
}
results = []
for destination in destinations:
try:
result = self._publish_to_destination(content, destination, account)
results.append(result)
except Exception as e:
logger.error(
f"Error publishing content {content_id} to {destination}: {str(e)}",
exc_info=True
)
results.append({
'destination': destination,
'success': False,
'error': str(e)
})
return {
'success': all(r.get('success', False) for r in results),
'results': results
}
def _publish_to_destination(
self,
content,
destination: str,
account
) -> Dict[str, Any]:
"""
Publish content to a specific destination.
Args:
content: Content instance
destination: Destination platform name
account: Account instance
Returns:
dict: Publishing result
"""
# Create publishing record
record = PublishingRecord.objects.create(
account=account,
site=content.site,
sector=content.sector,
content=content,
destination=destination,
status='pending'
)
try:
# Get adapter for destination
adapter = self._get_adapter(destination)
if not adapter:
raise ValueError(f"No adapter found for destination: {destination}")
# Publish via adapter
result = adapter.publish(content, {'account': account})
# Update record
record.status = 'published' if result.get('success') else 'failed'
record.destination_id = result.get('external_id')
record.destination_url = result.get('url')
record.published_at = result.get('published_at')
record.error_message = result.get('error')
record.metadata = result.get('metadata', {})
record.save()
return {
'destination': destination,
'success': result.get('success', False),
'publishing_record_id': record.id,
'external_id': result.get('external_id'),
'url': result.get('url')
}
except Exception as e:
record.status = 'failed'
record.error_message = str(e)
record.save()
raise
def _get_adapter(self, destination: str):
"""
Get adapter for destination platform.
Args:
destination: Platform name ('wordpress', 'sites', 'shopify')
Returns:
Adapter instance or None
"""
# Lazy import to avoid circular dependencies
if destination == 'sites':
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
elif destination == 'shopify':
# Will be implemented in Phase 6
return None
return None