QuentinL52 commited on
Commit
0f24635
·
verified ·
1 Parent(s): 154287e

Upload 13 files

Browse files
main.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from fastapi import FastAPI
3
+ from pydantic import BaseModel
4
+ from src.core.config import settings
5
+ from src.services.user_router import router as user_router
6
+ from src.services.cv_router import router as cv_router
7
+ from src.services.interview_history_router import router as interview_history_router
8
+ from src.services.feedback_router import router as feedback_router
9
+
10
+ app = FastAPI(
11
+ title="Data Access API",
12
+ description="API for accessing data from MongoDB and PostgreSQL.",
13
+ version="1.0.0",
14
+ docs_url="/docs",
15
+ redoc_url="/redoc"
16
+ )
17
+
18
+ app.include_router(user_router, prefix="/api/v1", tags=["Users"])
19
+ app.include_router(cv_router, prefix="/api/v1", tags=["CVs"])
20
+ app.include_router(interview_history_router, prefix="/api/v1", tags=["Interview Histories"])
21
+ app.include_router(feedback_router, prefix="/api/v1", tags=["Feedbacks"])
22
+
23
+ class HealthCheck(BaseModel):
24
+ status: str = "ok"
25
+
26
+ @app.get("/", response_model=HealthCheck, tags=["Status"])
27
+ async def health_check():
28
+ return HealthCheck()
29
+
30
+ if __name__ == "__main__":
31
+ import uvicorn
32
+ port = int(os.getenv("PORT", 8003)) # Use PORT environment variable, default to 8003
33
+ uvicorn.run(app, host="0.0.0.0", port=port)
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ pydantic
4
+ pydantic-settings
5
+ motor
6
+ sqlalchemy
7
+ psycopg2-binary
8
+ python-dotenv
src/core/config.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings
2
+ from dotenv import load_dotenv
3
+
4
+ class Settings(BaseSettings):
5
+ # MongoDB
6
+ MONGO_URI: str
7
+ MONGO_DB_NAME: str
8
+ MONGO_CV_COLLECTION: str
9
+ MONGO_INTERVIEW_COLLECTION: str
10
+ MONGO_FEEDBACK_COLLECTION: str
11
+
12
+ # PostgreSQL
13
+ DATABASE_URL: str
14
+ ASYNC_DATABASE_URL: str
15
+
16
+ class Config:
17
+ env_file = ".env"
18
+
19
+ load_dotenv()
20
+ settings = Settings()
src/core/database.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from motor.motor_asyncio import AsyncIOMotorClient
2
+ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
3
+ from sqlalchemy.orm import sessionmaker
4
+ from src.core.config import settings
5
+ from sqlalchemy.pool import NullPool
6
+
7
+ # MongoDB client
8
+ mongo_client = AsyncIOMotorClient(settings.MONGO_URI)
9
+ mongo_db = mongo_client[settings.MONGO_DB_NAME]
10
+
11
+
12
+ connect_args = {"statement_cache_size": 0}
13
+
14
+ engine = create_async_engine(
15
+ str(settings.ASYNC_DATABASE_URL),
16
+ poolclass=NullPool,
17
+ connect_args=connect_args,
18
+ execution_options={"compiled_cache": None},
19
+ )
20
+
21
+ SessionLocal = sessionmaker(
22
+ autocommit=False,
23
+ autoflush=False,
24
+ bind=engine,
25
+ class_=AsyncSession,
26
+ expire_on_commit=False,
27
+ )
28
+
29
+ async def get_db():
30
+ async with SessionLocal() as session:
31
+ yield session
src/models/mongo/base.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from motor.motor_asyncio import AsyncIOMotorDatabase
2
+ from pydantic import BaseModel
3
+ from bson import ObjectId
4
+
5
+ class BaseMongoModel(BaseModel):
6
+ id: str | None = None
7
+
8
+ @classmethod
9
+ async def get(cls, db: AsyncIOMotorDatabase, collection: str, query: dict):
10
+ return await db[collection].find_one(query)
11
+
12
+ @classmethod
13
+ async def get_all(cls, db: AsyncIOMotorDatabase, collection: str, query: dict = {}):
14
+ return await db[collection].find(query).to_list(1000)
15
+
16
+ @classmethod
17
+ async def create(cls, db: AsyncIOMotorDatabase, collection: str, data: dict):
18
+ result = await db[collection].insert_one(data)
19
+ return str(result.inserted_id)
20
+
21
+ @classmethod
22
+ async def update(cls, db: AsyncIOMotorDatabase, collection: str, query: dict, data: dict):
23
+ await db[collection].update_one(query, {"$set": data})
24
+
25
+ @classmethod
26
+ async def delete(cls, db: AsyncIOMotorDatabase, collection: str, query: dict):
27
+ await db[collection].delete_one(query)
src/models/mongo/cv_model.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import Field
2
+ from app.models.mongo.base import BaseMongoModel
3
+ from app.config import settings
4
+
5
+ class CVModel(BaseMongoModel):
6
+ collection_name: str = settings.MONGO_CV_COLLECTION
7
+
8
+ user_id: str | None = None
9
+ parsed_data: dict = Field(default_factory=dict)
10
+ raw_text: str | None = None
11
+ upload_date: str | None = None # ISO format string
src/models/mongo/feedback_model.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import Field
2
+ from app.models.mongo.base import BaseMongoModel
3
+ from app.config import settings
4
+
5
+ class FeedbackModel(BaseMongoModel):
6
+ collection_name: str = settings.MONGO_FEEDBACK_COLLECTION
7
+
8
+ user_id: str | None = None
9
+ interview_id: str | None = None
10
+ feedback_content: dict = Field(default_factory=dict)
11
+ feedback_date: str | None = None # ISO format string
src/models/mongo/interview_history_model.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import Field
2
+ from app.models.mongo.base import BaseMongoModel
3
+ from app.config import settings
4
+
5
+ class InterviewHistoryModel(BaseMongoModel):
6
+ collection_name: str = settings.MONGO_INTERVIEW_COLLECTION
7
+
8
+ user_id: str | None = None
9
+ cv_id: str | None = None
10
+ conversation: list[dict] = Field(default_factory=list) # List of {role: str, content: str}
11
+ start_time: str | None = None # ISO format string
12
+ end_time: str | None = None # ISO format string
src/models/postgres/user_model.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, String, DateTime, Boolean, JSON
2
+ from sqlalchemy.ext.declarative import declarative_base
3
+ from datetime import datetime
4
+
5
+ Base = declarative_base()
6
+
7
+ class User(Base):
8
+ __tablename__ = "user"
9
+
10
+ id = Column(Integer, primary_key=True, index=True, autoincrement=True)
11
+ google_id = Column(String, unique=True, nullable=True, index=True)
12
+ email = Column(String, unique=True, index=True, nullable=False)
13
+ name = Column(String, nullable=True)
14
+ picture_url = Column(String, nullable=True)
15
+ candidate_mongo_id = Column(String, nullable=True)
16
+ created_at = Column(DateTime, nullable=True)
17
+ auth_providers = Column(JSON, default=list)
18
+ hashed_password = Column(String, nullable=True)
19
+ is_active = Column(Boolean, default=True)
20
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
21
+ last_login = Column(DateTime, nullable=True)
src/services/cv_router.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from motor.motor_asyncio import AsyncIOMotorDatabase
3
+ from src.core.database import mongo_db
4
+ from src.models.mongo.cv_model import CVModel
5
+ from pydantic import BaseModel
6
+ from typing import Optional, Dict, Any
7
+ from bson import ObjectId
8
+
9
+ router = APIRouter()
10
+
11
+ class CVCreate(BaseModel):
12
+ user_id: str
13
+ parsed_data: Dict[str, Any]
14
+ raw_text: Optional[str] = None
15
+ upload_date: str
16
+
17
+ class CVResponse(BaseModel):
18
+ id: str = Field(alias="_id")
19
+ user_id: str
20
+ parsed_data: Dict[str, Any]
21
+ raw_text: Optional[str] = None
22
+ upload_date: str
23
+
24
+ class Config:
25
+ populate_by_name = True
26
+ json_encoders = {ObjectId: str}
27
+
28
+ @router.post("/cvs", response_model=CVResponse)
29
+ async def create_cv(cv: CVCreate, db: AsyncIOMotorDatabase = Depends(lambda: mongo_db)):
30
+ cv_entry = CVModel(**cv.model_dump(by_alias=True))
31
+ cv_id = await CVModel.create(db, CVModel.collection_name, cv_entry.model_dump(exclude_unset=True))
32
+ cv_entry.id = cv_id
33
+ return cv_entry
34
+
35
+ @router.get("/cvs/{cv_id}", response_model=CVResponse)
36
+ async def get_cv_by_id(cv_id: str, db: AsyncIOMotorDatabase = Depends(lambda: mongo_db)):
37
+ cv = await CVModel.get(db, CVModel.collection_name, {"_id": cv_id})
38
+ if cv is None:
39
+ raise HTTPException(status_code=404, detail="CV not found")
40
+ return cv
src/services/feedback_router.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from motor.motor_asyncio import AsyncIOMotorDatabase
3
+ from src.core.database import mongo_db
4
+ from src.models.mongo.feedback_model import FeedbackModel
5
+ from pydantic import BaseModel, Field
6
+ from typing import Optional, Dict, Any
7
+ from bson import ObjectId
8
+
9
+ router = APIRouter()
10
+
11
+ class FeedbackCreate(BaseModel):
12
+ user_id: str
13
+ interview_id: str
14
+ feedback_content: Dict[str, Any]
15
+ feedback_date: str
16
+
17
+ class FeedbackResponse(BaseModel):
18
+ id: str = Field(alias="_id")
19
+ user_id: str
20
+ interview_id: str
21
+ feedback_content: Dict[str, Any]
22
+ feedback_date: str
23
+
24
+ class Config:
25
+ populate_by_name = True
26
+ json_encoders = {ObjectId: str}
27
+
28
+ @router.post("/feedbacks", response_model=FeedbackResponse)
29
+ async def create_feedback(feedback: FeedbackCreate, db: AsyncIOMotorDatabase = Depends(lambda: mongo_db)):
30
+ feedback_entry = FeedbackModel(**feedback.model_dump(by_alias=True))
31
+ feedback_id = await FeedbackModel.create(db, FeedbackModel.collection_name, feedback_entry.model_dump(exclude_unset=True))
32
+ feedback_entry.id = feedback_id
33
+ return feedback_entry
34
+
35
+ @router.get("/feedbacks/{feedback_id}", response_model=FeedbackResponse)
36
+ async def get_feedback_by_id(feedback_id: str, db: AsyncIOMotorDatabase = Depends(lambda: mongo_db)):
37
+ feedback = await FeedbackModel.get(db, FeedbackModel.collection_name, {"_id": feedback_id})
38
+ if feedback is None:
39
+ raise HTTPException(status_code=404, detail="Feedback not found")
40
+ return feedback
src/services/interview_history_router.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from motor.motor_asyncio import AsyncIOMotorDatabase
3
+ from src.core.database import mongo_db
4
+ from src.models.mongo.interview_history_model import InterviewHistoryModel
5
+ from pydantic import BaseModel, Field
6
+ from typing import Optional, List, Dict, Any
7
+ from bson import ObjectId
8
+
9
+ router = APIRouter()
10
+
11
+ class InterviewHistoryCreate(BaseModel):
12
+ user_id: str
13
+ cv_id: str
14
+ job_offer_id: str
15
+ conversation: List[Dict[str, Any]]
16
+ start_time: str
17
+ end_time: Optional[str] = None
18
+
19
+ class InterviewHistoryUpdate(BaseModel):
20
+ conversation: Optional[List[Dict[str, Any]]] = None
21
+ end_time: Optional[str] = None
22
+
23
+ class InterviewHistoryResponse(BaseModel):
24
+ id: str = Field(alias="_id")
25
+ user_id: str
26
+ cv_id: str
27
+ job_offer_id: str
28
+ conversation: List[Dict[str, Any]]
29
+ start_time: str
30
+ end_time: Optional[str] = None
31
+
32
+ class Config:
33
+ populate_by_name = True
34
+ json_encoders = {ObjectId: str}
35
+
36
+ @router.post("/interview-histories", response_model=InterviewHistoryResponse)
37
+ async def create_interview_history(history: InterviewHistoryCreate, db: AsyncIOMotorDatabase = Depends(lambda: mongo_db)):
38
+ history_entry = InterviewHistoryModel(**history.model_dump(by_alias=True))
39
+ history_id = await InterviewHistoryModel.create(db, InterviewHistoryModel.collection_name, history_entry.model_dump(exclude_unset=True))
40
+ history_entry.id = history_id
41
+ return history_entry
42
+
43
+ @router.get("/interview-histories/{history_id}", response_model=InterviewHistoryResponse)
44
+ async def get_interview_history_by_id(history_id: str, db: AsyncIOMotorDatabase = Depends(lambda: mongo_db)):
45
+ history = await InterviewHistoryModel.get(db, InterviewHistoryModel.collection_name, {"_id": history_id})
46
+ if history is None:
47
+ raise HTTPException(status_code=404, detail="Interview history not found")
48
+ return history
49
+
50
+ @router.put("/interview-histories/{history_id}", response_model=InterviewHistoryResponse)
51
+ async def update_interview_history(history_id: str, history: InterviewHistoryUpdate, db: AsyncIOMotorDatabase = Depends(lambda: mongo_db)):
52
+ await InterviewHistoryModel.update(db, InterviewHistoryModel.collection_name, {"_id": history_id}, history.model_dump(exclude_unset=True))
53
+ return await get_interview_history_by_id(history_id, db)
src/services/user_router.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+ from sqlalchemy import select, update
4
+ from src.core.database import get_db
5
+ from src.models.postgres.user_model import User
6
+ from pydantic import BaseModel
7
+ from typing import Optional
8
+
9
+ router = APIRouter()
10
+
11
+ class UserCreate(BaseModel):
12
+ email: str
13
+ hashed_password: str
14
+ name: Optional[str] = None
15
+ picture_url: Optional[str] = None
16
+ google_id: Optional[str] = None
17
+ candidate_mongo_id: Optional[str] = None
18
+
19
+ class UserUpdate(BaseModel):
20
+ email: Optional[str] = None
21
+ hashed_password: Optional[str] = None
22
+ name: Optional[str] = None
23
+ picture_url: Optional[str] = None
24
+ google_id: Optional[str] = None
25
+ candidate_mongo_id: Optional[str] = None
26
+
27
+ class UserResponse(BaseModel):
28
+ id: int
29
+ email: str
30
+ name: Optional[str] = None
31
+ picture_url: Optional[str] = None
32
+ google_id: Optional[str] = None
33
+ candidate_mongo_id: Optional[str] = None
34
+
35
+ class Config:
36
+ from_attributes = True
37
+
38
+ @router.post("/users", response_model=UserResponse)
39
+ async def create_user(user: UserCreate, db: AsyncSession = Depends(get_db)):
40
+ db_user = User(**user.model_dump())
41
+ db.add(db_user)
42
+ await db.commit()
43
+ await db.refresh(db_user)
44
+ return db_user
45
+
46
+ @router.get("/users/{user_id}", response_model=UserResponse)
47
+ async def get_user_by_id(user_id: int, db: AsyncSession = Depends(get_db)):
48
+ result = await db.execute(select(User).where(User.id == user_id))
49
+ user = result.scalar_one_or_none()
50
+ if user is None:
51
+ raise HTTPException(status_code=404, detail="User not found")
52
+ return user
53
+
54
+ @router.get("/users/email/{email}", response_model=UserResponse)
55
+ async def get_user_by_email(email: str, db: AsyncSession = Depends(get_db)):
56
+ result = await db.execute(select(User).where(User.email == email))
57
+ user = result.scalar_one_or_none()
58
+ if user is None:
59
+ raise HTTPException(status_code=404, detail="User not found")
60
+ return user
61
+
62
+ @router.put("/users/{user_id}", response_model=UserResponse)
63
+ async def update_user(user_id: int, user: UserUpdate, db: AsyncSession = Depends(get_db)):
64
+ query = update(User).where(User.id == user_id).values(**user.model_dump(exclude_unset=True))
65
+ await db.execute(query)
66
+ await db.commit()
67
+ return await get_user_by_id(user_id, db)