from datetime import date, time import os import httpx from dotenv import load_dotenv from fastapi import HTTPException from sqlalchemy.ext.asyncio import AsyncSession from pydantic import BaseModel, model_validator from job.job_post.models import JobPosts from job.job_post.plugins import ( BufferError, create_buffer_post, list_buffer_channels, local_status, normalize_platform, parse_buffer_datetime, render_job_post, resolve_channel, ) from job.job_post.serializers import serialize_job_post load_dotenv() class JobPostCreate(BaseModel): title: str experience_min: int | None = None experience_max: int | None = None requirements: list[str] = [] optional_skills: list[str] = [] salary: str = "Anonymous" location: str | None = None employment_type: str | None = None platform: str = "linkedin" description: str | None = None platform: str | None = None channel_id: str | None = None mode: str = "addToQueue" scheduler_time: time | None = time(0, 0, 0) scheduler_date: date | None = None due_at: str | None = None @model_validator(mode="after") def validate_mode_and_due_at(self): allowed = {"addToQueue", "shareNow", "customScheduled"} if self.mode not in allowed: raise ValueError(f"mode must be one of {sorted(allowed)}") if self.mode == "customScheduled" and not self.due_at: raise ValueError("due_at is required when mode is customScheduled") return self class JobPost: def __init__(self,session:AsyncSession): self.session=session self.buffer_api=os.getenv("BUFFER_API") self.channel_id=os.getenv("BUFFER_CHANNEL_ID") async def _resolve_target(self,payload): """Pick the Buffer channel to post to, and the service it belongs to. Precedence: an explicit channel_id, then the requested platform, then the configured default channel. Returns (channel_id, service) where service is None if we did not have to look the channel up. """ if payload.get("channel_id"): return payload["channel_id"],None if payload.get("platform"): channel=await resolve_channel(payload["platform"]) return channel["id"],channel.get("service") if self.channel_id: return self.channel_id,None raise HTTPException( status_code=400, detail="Provide channel_id or platform, or configure BUFFER_CHANNEL_ID", ) async def post_job(self,payload,current_user): try: channel_id,service=await self._resolve_target(payload) except (httpx.HTTPError,BufferError,RuntimeError) as e: raise HTTPException(status_code=502,detail=f"Failed to resolve Buffer channel: {e}") from e text=render_job_post(payload) fields={ "title":payload.get("title"), "employment_type":payload.get("employment_type"), "location":payload.get("location"), "experience_min":payload.get("experience_min"), "experience_max":payload.get("experience_max"), "requirements":list(payload.get("requirements") or []), "optional_skills":list(payload.get("optional_skills") or []), "salary":payload.get("salary") or "Anonymous", "description":payload.get("description"), "post_text":text, "channel_id":channel_id, "status":"draft", "created_by":current_user["id"], } # Only set platform when it is actually known: passing None would override the # column default and break the NOT NULL constraint. Buffer's channelService # replaces this with the authoritative value once the post is created. known_platform=service or normalize_platform(payload.get("platform")) if known_platform: fields["platform"]=known_platform row=await JobPosts.insert_job_post(self.session,fields) try: post=await create_buffer_post( text, channel_id, mode=payload.get("mode") or "addToQueue", due_at=payload.get("due_at"), ) except (httpx.HTTPError,BufferError,RuntimeError) as e: await JobPosts.mark_failed(self.session,str(row.id),str(e)) raise HTTPException(status_code=502,detail=f"Failed to publish job post to Buffer: {e}") from e saved=await JobPosts.mark_buffer_result( self.session, str(row.id), buffer_post_id=post["id"], status=local_status(post.get("status")), external_link=post.get("externalLink"), sent_at=parse_buffer_datetime(post.get("sentAt")), platform=post.get("channelService"), ) return serialize_job_post(saved) async def list_channels(self): try: return await list_buffer_channels() except (httpx.HTTPError,BufferError,RuntimeError) as e: raise HTTPException(status_code=502,detail="Failed to list Buffer channels") from e async def fetch_job_posts(self,search=None,top=None,skip=0,ids=None,active_only=True): rows,total=await JobPosts.fetch_job_posts( self.session, search=search, top=top, skip=skip, ids=ids, active_only=active_only, ) return [serialize_job_post(r) for r in rows],total