Spaces:
Sleeping
Sleeping
Upload 13 files
Browse files- main.py +33 -0
- requirements.txt +8 -0
- src/core/config.py +20 -0
- src/core/database.py +31 -0
- src/models/mongo/base.py +27 -0
- src/models/mongo/cv_model.py +11 -0
- src/models/mongo/feedback_model.py +11 -0
- src/models/mongo/interview_history_model.py +12 -0
- src/models/postgres/user_model.py +21 -0
- src/services/cv_router.py +40 -0
- src/services/feedback_router.py +40 -0
- src/services/interview_history_router.py +53 -0
- src/services/user_router.py +67 -0
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)
|