From da3077c302c2888415ae7785f863a521c0939123 Mon Sep 17 00:00:00 2001 From: "ahmed.mujtaba" Date: Mon, 10 Aug 2026 21:01:51 +0500 Subject: [PATCH] talet pool api done --- backend/Dockerfile | 3 +- backend/inbox/app.py | 2 + ...ehdi Raza Content Writing Samples 001.docx | Bin 0 -> 13095 bytes backend/inbox/models.py | 65 ++++++++++++------ backend/inbox/plugins.py | 9 ++- backend/inbox/sync_tasks.py | 7 +- backend/inbox/views.py | 4 +- backend/job/app.py | 38 +++++----- backend/job/candidate/serializers.py | 31 +++++++++ backend/job/candidate/views.py | 52 +++++++++++--- backend/job/job_post/serializers.py | 2 + backend/users/models.py | 12 +++- docker-compose.yml | 12 ++++ 13 files changed, 181 insertions(+), 56 deletions(-) create mode 100644 backend/inbox/decoded_attachments/Mehdi Raza Content Writing Samples 001.docx diff --git a/backend/Dockerfile b/backend/Dockerfile index e462a2e..3a584e1 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,6 +1,7 @@ FROM python:3.12-slim WORKDIR /app +ENV PYTHONPATH=/app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt @@ -9,4 +10,4 @@ COPY . . # Runs the Taskiq worker against taskiq_management.broker_setup. # docker-compose overrides this command if needed. -CMD ["taskiq", "worker", "taskiq_management.broker_setup:broker", "inbox.tasks", "taskiq_management.tasks"] +CMD ["taskiq", "worker", "taskiq_management.broker_setup:broker", "inbox.tasks", "inbox.sync_tasks", "taskiq_management.tasks"] diff --git a/backend/inbox/app.py b/backend/inbox/app.py index 6d4321e..9f279c5 100644 --- a/backend/inbox/app.py +++ b/backend/inbox/app.py @@ -35,6 +35,8 @@ async def fetch_email( await service.enqueue_matching(list(service.pending_match_ids),force=False) account_setup=[] + if test_on: + return JSONResponse(content={"data":items_lst,"status_code":200}) if service.pending_confirmation_emails: account_setup=await service.send_account_setup(list(service.pending_confirmation_emails)) diff --git a/backend/inbox/decoded_attachments/Mehdi Raza Content Writing Samples 001.docx b/backend/inbox/decoded_attachments/Mehdi Raza Content Writing Samples 001.docx new file mode 100644 index 0000000000000000000000000000000000000000..96dd43c7f7f379721684e1dfceee17ba53044c6a GIT binary patch literal 13095 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 h=Lr5wzA*fM52U;_#P6*I0APN the raw round dict.""" +async def fetch_read_status_delta(folder, since=None, limit=100, max_pages=10, token=None): + """GET /sync/read-status -> the raw round dict. + + Upstream Email API caps `limit` at 100; keep the default at that ceiling. + """ if not EMAIL_URL: raise RuntimeError("EMAIL_URL must be set") auth_token=token or EMAIL_API_TOKEN if not auth_token: raise RuntimeError("EMAIL_API_TOKEN must be set") - params={"folder":folder,"limit":limit,"max_pages":max_pages} + params={"folder":folder,"limit":min(int(limit or 100),100),"max_pages":max_pages} if since: params["since"]=since async with httpx.AsyncClient(timeout=15.0) as client: diff --git a/backend/inbox/sync_tasks.py b/backend/inbox/sync_tasks.py index 42dc6ac..42cc8e9 100644 --- a/backend/inbox/sync_tasks.py +++ b/backend/inbox/sync_tasks.py @@ -49,9 +49,14 @@ async def sync_read_status() -> dict: round_data=await fetch_read_status_delta( EMAIL_SYNC_FOLDER, since=since if rounds==1 else None, - limit=1000, + limit=100, max_pages=10, ) + except httpx.ConnectError as e: + # Email API down / unreachable from this process — soft-fail so the + # cron does not burn retries every minute. + logger.warning("sync_read_status unreachable: %s",e) + return {"error":"unreachable","detail":str(e)} except httpx.HTTPStatusError as e: if e.response.status_code==401: logger.warning("sync_read_status 401 — device-code sign-in required") diff --git a/backend/inbox/views.py b/backend/inbox/views.py index 8c6092d..ea6a12d 100644 --- a/backend/inbox/views.py +++ b/backend/inbox/views.py @@ -51,7 +51,7 @@ class Email: except Exception as e: raise HTTPException(status_code=500,detail=str(e)) - async def get_email_by_id(self,message_id): + async def get_email_by_id(self,message_id,test_on=True): async with httpx.AsyncClient() as client: try: response=await client.get(f"{self.get_url}/emails/{message_id}", @@ -63,6 +63,8 @@ class Email: row,new_user_email=await Inbox_Messages.insert_email(session=self.session,email_data=data,file_path=re_create_file) if row.attachment and row.file_path and row.match_status is None: self.pending_match_ids.append(str(row.id)) + if test_on: + return data if new_user_email: self.pending_confirmation_emails.append(new_user_email) return data diff --git a/backend/job/app.py b/backend/job/app.py index 682bb75..3d95117 100644 --- a/backend/job/app.py +++ b/backend/job/app.py @@ -2,7 +2,7 @@ from fastapi import APIRouter,Depends,Query from fastapi.responses import JSONResponse from fastapi import HTTPException from db_setup import get_session -from job.candidate.views import FileRead +from job.candidate.views import FileRead,CandidateView from sqlalchemy.ext.asyncio import AsyncSession from users.permissions import PermissionTag, require_permission from job.job_post.views import JobPost,JobPostCreate @@ -11,6 +11,7 @@ from job.job_post.plugins import PlatformAlias from fastapi import UploadFile, File from dotenv import load_dotenv from datetime import datetime, time, timezone + load_dotenv() logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -102,21 +103,20 @@ async def buffer_channels( except Exception as e: raise HTTPException(status_code=500,detail=str(e)) -# @router.get("/candidate/fetch") -# async def fetch_candidate( -# user_id:str=Query(None), -# current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)), -# session: AsyncSession = Depends(get_session), -# ): -# try: -# service=CandidateView(session=session) -# if user_id: -# data=await service.get_candidate(user_id=user_id) -# else: -# data=await service.get_candidate() -# return JSONResponse(content={"data":data,"status_code":200}) -# except HTTPException: -# raise -# except Exception as e: -# raise HTTPException(status_code=500,detail=str(e)) - \ No newline at end of file +@router.get("/candidate/fetch") +async def fetch_candidate( + user_id:str=Query(None), + limit:int=Query(10), + offset:int=Query(0), + current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)), + session: AsyncSession = Depends(get_session), +): + try: + service=CandidateView(session=session) + data=await service.get_candidate(user_id=user_id,limit=limit,offset=offset) + total=len(data) if isinstance(data,list) else 1 + return JSONResponse(content={"data":data,"total":total,"status_code":200}) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500,detail=str(e)) diff --git a/backend/job/candidate/serializers.py b/backend/job/candidate/serializers.py index e69de29..9e6f6b7 100644 --- a/backend/job/candidate/serializers.py +++ b/backend/job/candidate/serializers.py @@ -0,0 +1,31 @@ +from inbox.models import Inbox +from typing import Any,List,Dict + + +def serialize_candidate_profile(link:Inbox|List[Inbox]|Dict[str,Any]|List[Dict[str,Any]]) -> Dict[str,Any]|List[Dict[str,Any]]: + if isinstance(link,list): + return [serialize_candidate_profile(item) for item in link] + if isinstance(link,dict): + return link + + user = link.user + message = link.messages + return { + "inbox_id": link.id, + "user_id": str(link.user_id) if link.user_id else None, + "name": user.name if user else None, + "email": user.email if user else None, + "is_active": user.is_active if user else None, + "message_id": str(link.message_id) if link.message_id else None, + "created_at": link.created_at.isoformat() if link.created_at else None, + "application_status": message.application_status if message else None, + "experience": message.experience if message else None, + "resume_text": message.resume_text if message else None, + "suggested_job_post_ids": list(message.suggested_job_post_ids or []) if message else [], + "match_summary": message.match_summary if message else None, + "match_reasoning": message.match_reasoning if message else None, + "match_status": message.match_status if message else None, + "match_error": message.match_error if message else None, + "matched_at": message.matched_at.isoformat() if message and message.matched_at else None, + "job_posts": [], + } diff --git a/backend/job/candidate/views.py b/backend/job/candidate/views.py index 6b10aa3..6f430c7 100644 --- a/backend/job/candidate/views.py +++ b/backend/job/candidate/views.py @@ -5,7 +5,10 @@ from fastapi import HTTPException from pypdf import PdfReader from sqlalchemy import select from sqlmodel import true -from inbox.models import Inbox_Messages +from job.job_post.models import JobPosts +from job.job_post.serializers import serialize_job_post +from job.candidate.serializers import serialize_candidate_profile +from inbox.models import Inbox_Messages,Inbox from job.candidate.plugins import normalize_spaced_text class FileRead: @@ -68,12 +71,41 @@ class FileRead: # get_subject=Inbox_Messages.candidate_x_inbox(self.session,self.candidate_id) # get_file= -# class CandidateView: -# def __init__(self,session:AsyncSession): -# self.session=session - -# async def get_candidate(self,user_id=None): -# try: -# call_func=Inbox_Messages.get_candidate_profile(user_id=user_id) -# except Exception as e: -# raise HTTPException(status_code=500,detail=str(e)) \ No newline at end of file +class CandidateView: + def __init__(self,session:AsyncSession): + self.session=session + + async def get_candidate(self,user_id=None,limit=10,offset=0): + try: + rows=await Inbox.get_candidate_profile(session=self.session,user_id=user_id,limit=limit,offset=offset) + return await self.attach_job_posts(rows) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500,detail=str(e)) + + async def get_job_post_by_id(self,record_id,data=None): + """Load full job_posts row and optionally append it onto a candidate payload.""" + try: + job_post_data=await JobPosts.get_job_post_by_id(session=self.session,record_id=record_id) + if not job_post_data: + return None + payload=serialize_job_post(job_post_data) + if isinstance(data,dict): + data.setdefault("job_posts",[]).append(payload) + return payload + except Exception as e: + raise HTTPException(status_code=500,detail=str(e)) + + async def attach_job_posts(self,data): + """Normalize list/single, serialize each record, attach full job_posts rows.""" + single=not isinstance(data,list) + records=[data] if single else list(data or []) + enriched=[] + for record in records: + payload=serialize_candidate_profile(record) + payload["job_posts"]=[] + for job_id in payload.get("suggested_job_post_ids") or []: + await self.get_job_post_by_id(record_id=job_id,data=payload) + enriched.append(payload) + return enriched[0] if single else enriched diff --git a/backend/job/job_post/serializers.py b/backend/job/job_post/serializers.py index 4589f3d..d58e3ad 100644 --- a/backend/job/job_post/serializers.py +++ b/backend/job/job_post/serializers.py @@ -13,6 +13,8 @@ def serialize_job_post(row) -> dict: "post_text": row.post_text, "channel_id": row.channel_id, "platform": row.platform, + "is_active": row.is_active, + "is_deleted": row.is_deleted, "buffer_post_id": row.buffer_post_id, "buffer_external_link": row.buffer_external_link, "buffer_sent_at": row.buffer_sent_at.isoformat() if row.buffer_sent_at else None, diff --git a/backend/users/models.py b/backend/users/models.py index cacf22d..bbfa0a1 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -1,5 +1,6 @@ import uuid from datetime import datetime +from typing import TYPE_CHECKING from sqlalchemy import func, or_ from sqlalchemy.ext.asyncio import AsyncSession @@ -9,6 +10,9 @@ from sqlmodel import Field, Relationship, SQLModel, select from role.models import Roles from job.job_post.models import JobPosts +if TYPE_CHECKING: # runtime import would cycle: inbox.models imports this module + from inbox.models import Inbox + class Users(SQLModel, table=True): __tablename__ = "users" @@ -16,7 +20,9 @@ class Users(SQLModel, table=True): name: str email: str = Field(unique=True) role_id: int | None = Field(nullable=True, foreign_key="roles.id") - role: Roles | None = Relationship(back_populates="users") + role: Roles | None = Relationship(back_populates="users", + sa_relationship_kwargs={"lazy": "selectin"} + ) # selectin, not joined: this is a one-to-many, so a joined load would repeat the # user row once per post. Without an explicit strategy the default is a lazy load, # which raises MissingGreenlet the moment anything touches it under asyncio. @@ -24,6 +30,10 @@ class Users(SQLModel, table=True): back_populates="user", sa_relationship_kwargs={"lazy": "selectin"}, ) + inbox: list["Inbox"] = Relationship( + back_populates="user", + sa_relationship_kwargs={"lazy": "selectin"}, + ) password: str created_at: datetime = Field(default_factory=datetime.now) updated_at: datetime = Field(default_factory=datetime.now) diff --git a/docker-compose.yml b/docker-compose.yml index be849d0..7f99556 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,7 @@ services: build: context: ./backend container_name: hrms-taskiq-worker + working_dir: /app command: [ "taskiq", @@ -32,10 +33,14 @@ services: env_file: - ./backend/.env environment: + PYTHONPATH: /app REDIS_URL: redis://redis:6379/0 TASKIQ_QUEUE_NAME: inbox TASKIQ_WORKER_NAME: worker-01 + # .env uses localhost for the host-side API; containers must reach the host. DB_HOST: host.docker.internal + EMAIL_URL: http://host.docker.internal:5000 + BACKEND_URL: http://host.docker.internal:8000 extra_hosts: - "host.docker.internal:host-gateway" volumes: @@ -49,12 +54,19 @@ services: build: context: ./backend container_name: hrms-taskiq-scheduler + working_dir: /app command: ["taskiq", "scheduler", "taskiq_management.broker_setup:scheduler", "inbox.sync_tasks"] env_file: - ./backend/.env environment: + PYTHONPATH: /app REDIS_URL: redis://redis:6379/0 TASKIQ_QUEUE_NAME: inbox + DB_HOST: host.docker.internal + EMAIL_URL: http://host.docker.internal:5000 + BACKEND_URL: http://host.docker.internal:8000 + extra_hosts: + - "host.docker.internal:host-gateway" depends_on: redis: condition: service_healthy