forked from LiveCarta/ContentAutomation
Store posting results to a mongodb
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from content_automation.adapters.publish_store.mongodb import MongoPublishedContentStore
|
||||
|
||||
__all__ = ["MongoPublishedContentStore"]
|
||||
22
src/content_automation/adapters/publish_store/mongodb.py
Normal file
22
src/content_automation/adapters/publish_store/mongodb.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pymongo import MongoClient
|
||||
|
||||
from content_automation.interfaces import PublishedContentStore
|
||||
from content_automation.models import PublishedContentRecord
|
||||
|
||||
|
||||
class MongoPublishedContentStore(PublishedContentStore):
|
||||
"""MongoDB-backed persistence for publish operation records."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
connection_uri: str,
|
||||
database_name: str,
|
||||
collection_name: str,
|
||||
) -> None:
|
||||
self._client = MongoClient(connection_uri)
|
||||
self._collection = self._client[database_name][collection_name]
|
||||
|
||||
def save(self, record: PublishedContentRecord) -> None:
|
||||
self._collection.insert_one(record.model_dump(mode="json"))
|
||||
@@ -1,7 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from content_automation.adapters.storage.base import StorageAdapterBase
|
||||
from content_automation.interfaces import SocialNetworkAdapter
|
||||
from content_automation.interfaces import PublishedContentStore, SocialNetworkAdapter
|
||||
from content_automation.models import PublishedContentRecord
|
||||
from content_automation.settings import AppSettings
|
||||
|
||||
|
||||
@@ -13,22 +17,60 @@ class PublishController:
|
||||
settings: AppSettings,
|
||||
storage: StorageAdapterBase,
|
||||
social_adapters: dict[str, SocialNetworkAdapter],
|
||||
published_content_store: PublishedContentStore | None = None,
|
||||
) -> None:
|
||||
self._settings = settings
|
||||
self._storage = storage
|
||||
self._social_adapters = social_adapters
|
||||
self._published_content_store = published_content_store
|
||||
|
||||
def publish(self, relative_path: str, caption: str) -> dict[str, str]:
|
||||
def publish(self, relative_path: str, caption: str) -> dict[str, Any]:
|
||||
if not self._storage.exists(relative_path):
|
||||
raise FileNotFoundError(
|
||||
f"Media file is not available in storage: {relative_path}"
|
||||
)
|
||||
|
||||
media_url = self._storage.get_public_url(relative_path)
|
||||
result: dict[str, str] = {}
|
||||
result: dict[str, Any] = {}
|
||||
for network in self._settings.target_social_networks:
|
||||
adapter = self._social_adapters.get(network)
|
||||
if adapter is None:
|
||||
raise ValueError(f"No adapter configured for network: {network}")
|
||||
result[network] = adapter.post_media(media_url=media_url, caption=caption)
|
||||
|
||||
if self._published_content_store is not None:
|
||||
record = PublishedContentRecord(
|
||||
relative_path=relative_path,
|
||||
media_url=media_url,
|
||||
caption=caption,
|
||||
platform_responses=self._serialize_platform_responses(result),
|
||||
)
|
||||
self._published_content_store.save(record)
|
||||
|
||||
return result
|
||||
|
||||
def _serialize_platform_responses(
|
||||
self, responses: Mapping[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
serialized: dict[str, Any] = {}
|
||||
for network, response in responses.items():
|
||||
if network == "youtube":
|
||||
serialized[network] = self._extract_youtube_video_id(response)
|
||||
continue
|
||||
serialized[network] = response
|
||||
return serialized
|
||||
|
||||
@staticmethod
|
||||
def _extract_youtube_video_id(response: Any) -> str:
|
||||
if isinstance(response, str):
|
||||
return response
|
||||
|
||||
if isinstance(response, Mapping):
|
||||
video_id = response.get("id")
|
||||
if isinstance(video_id, str) and video_id:
|
||||
return video_id
|
||||
|
||||
raise ValueError(
|
||||
"Unsupported YouTube publish response: expected a string ID "
|
||||
"or payload containing an 'id' field."
|
||||
)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from content_automation.adapters.publish_store.mongodb import MongoPublishedContentStore
|
||||
from content_automation.adapters.social.instagram import InstagramAdapter
|
||||
from content_automation.adapters.social.youtube import YouTubeAdapter
|
||||
from content_automation.adapters.storage.base import StorageAdapterBase
|
||||
from content_automation.adapters.storage.local import LocalFilesystemStorageAdapter
|
||||
from content_automation.adapters.storage.s3 import S3StorageAdapter
|
||||
from content_automation.interfaces import SocialNetworkAdapter
|
||||
from content_automation.interfaces import PublishedContentStore, SocialNetworkAdapter
|
||||
from content_automation.settings import AppSettings
|
||||
|
||||
|
||||
@@ -49,3 +50,22 @@ def build_social_adapters(settings: AppSettings) -> dict[str, SocialNetworkAdapt
|
||||
expiry=settings.youtube.expiry or None,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def build_published_content_store(
|
||||
settings: AppSettings,
|
||||
) -> PublishedContentStore | None:
|
||||
if not settings.mongodb.enabled:
|
||||
return None
|
||||
|
||||
if not settings.mongodb.connection_uri:
|
||||
raise ValueError(
|
||||
"CONTENT_AUTOMATION_MONGODB__CONNECTION_URI must be set when "
|
||||
"MongoDB persistence is enabled."
|
||||
)
|
||||
|
||||
return MongoPublishedContentStore(
|
||||
connection_uri=settings.mongodb.connection_uri,
|
||||
database_name=settings.mongodb.database_name,
|
||||
collection_name=settings.mongodb.collection_name,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol
|
||||
from typing import Any, Protocol
|
||||
|
||||
from content_automation.models import PublishedContentRecord
|
||||
|
||||
|
||||
class SocialNetworkAdapter(Protocol):
|
||||
@@ -8,5 +10,12 @@ class SocialNetworkAdapter(Protocol):
|
||||
|
||||
name: str
|
||||
|
||||
def post_media(self, media_url: str, caption: str) -> str:
|
||||
"""Publish media and return an external post identifier."""
|
||||
def post_media(self, media_url: str, caption: str) -> Any:
|
||||
"""Publish media and return a provider response payload."""
|
||||
|
||||
|
||||
class PublishedContentStore(Protocol):
|
||||
"""Contract for persisting publish operation metadata."""
|
||||
|
||||
def save(self, record: PublishedContentRecord) -> None:
|
||||
"""Persist a published content record."""
|
||||
|
||||
@@ -3,7 +3,11 @@ from __future__ import annotations
|
||||
import argparse
|
||||
|
||||
from content_automation.controller import PublishController
|
||||
from content_automation.factories import build_social_adapters, build_storage_adapter
|
||||
from content_automation.factories import (
|
||||
build_published_content_store,
|
||||
build_social_adapters,
|
||||
build_storage_adapter,
|
||||
)
|
||||
from content_automation.settings import AppSettings
|
||||
|
||||
|
||||
@@ -23,8 +27,12 @@ def main() -> None:
|
||||
settings = AppSettings()
|
||||
storage = build_storage_adapter(settings)
|
||||
adapters = build_social_adapters(settings)
|
||||
published_content_store = build_published_content_store(settings)
|
||||
controller = PublishController(
|
||||
settings=settings, storage=storage, social_adapters=adapters
|
||||
settings=settings,
|
||||
storage=storage,
|
||||
social_adapters=adapters,
|
||||
published_content_store=published_content_store,
|
||||
)
|
||||
publish_results = controller.publish(
|
||||
relative_path=args.relative_path, caption=args.caption
|
||||
|
||||
16
src/content_automation/models.py
Normal file
16
src/content_automation/models.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class PublishedContentRecord(BaseModel):
|
||||
"""Stored metadata for a publish operation across social networks."""
|
||||
|
||||
relative_path: str
|
||||
media_url: str
|
||||
caption: str
|
||||
platform_responses: dict[str, Any]
|
||||
published_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
||||
@@ -48,9 +48,16 @@ class AppSettings(BaseSettings):
|
||||
local: LocalSettings = Field(default_factory=LocalSettings)
|
||||
s3: S3Settings = Field(default_factory=S3Settings)
|
||||
|
||||
class MongoDbSettings(BaseModel):
|
||||
enabled: bool = False
|
||||
connection_uri: str = ""
|
||||
database_name: str = "content_automation"
|
||||
collection_name: str = "published_content"
|
||||
|
||||
target_social_networks: list[str] = Field(
|
||||
default_factory=lambda: ["instagram", "youtube"]
|
||||
)
|
||||
instagram: InstagramSettings = Field(default_factory=InstagramSettings)
|
||||
youtube: YoutubeSettings = Field(default_factory=YoutubeSettings)
|
||||
storage: StorageSettings = Field(default_factory=StorageSettings)
|
||||
mongodb: MongoDbSettings = Field(default_factory=MongoDbSettings)
|
||||
|
||||
Reference in New Issue
Block a user