Spaces:
Running
Running
Commit ·
cf25e9f
1
Parent(s): 2d2110c
Backend i guess complete
Browse files- alembic/env.py +1 -0
- alembic/versions/0f71a23ab6a3_add_resume_text_to_users.py +32 -0
- alembic/versions/3436be625910_create_applications_table.py +44 -0
- app/cores/config.py +1 -1
- app/cores/database.py +1 -1
- app/cores/security.py +9 -11
- app/main.py +3 -1
- app/models/application.py +26 -0
- app/models/user.py +3 -2
- app/routers/application.py +122 -0
- app/routers/auth.py +2 -2
- app/routers/user.py +61 -4
- app/schemas/application.py +36 -0
- app/schemas/user.py +9 -4
- app/services/__init__.py +0 -0
- app/services/ai_service.py +48 -0
- app/services/resume_service.py +26 -0
- requirements.txt +0 -0
alembic/env.py
CHANGED
|
@@ -6,6 +6,7 @@ from alembic import context
|
|
| 6 |
from app.cores.config import settings
|
| 7 |
from app.models.user import User
|
| 8 |
config = context.config
|
|
|
|
| 9 |
|
| 10 |
config.set_main_option("sqlalchemy.url", f"postgresql://{settings.DATABASE_USERNAME}:{settings.DATABASE_PASSWORD}@{settings.DATABASE_HOSTNAME}:{settings.DATABASE_PORT}/{settings.DATABASE_NAME}")
|
| 11 |
|
|
|
|
| 6 |
from app.cores.config import settings
|
| 7 |
from app.models.user import User
|
| 8 |
config = context.config
|
| 9 |
+
from app.models.application import Application
|
| 10 |
|
| 11 |
config.set_main_option("sqlalchemy.url", f"postgresql://{settings.DATABASE_USERNAME}:{settings.DATABASE_PASSWORD}@{settings.DATABASE_HOSTNAME}:{settings.DATABASE_PORT}/{settings.DATABASE_NAME}")
|
| 12 |
|
alembic/versions/0f71a23ab6a3_add_resume_text_to_users.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""add resume_text to users
|
| 2 |
+
|
| 3 |
+
Revision ID: 0f71a23ab6a3
|
| 4 |
+
Revises: 3436be625910
|
| 5 |
+
Create Date: 2026-06-30 11:40:29.040562
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
from typing import Sequence, Union
|
| 9 |
+
|
| 10 |
+
from alembic import op
|
| 11 |
+
import sqlalchemy as sa
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# revision identifiers, used by Alembic.
|
| 15 |
+
revision: str = '0f71a23ab6a3'
|
| 16 |
+
down_revision: Union[str, Sequence[str], None] = '3436be625910'
|
| 17 |
+
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
+
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def upgrade() -> None:
|
| 22 |
+
"""Upgrade schema."""
|
| 23 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 24 |
+
op.add_column('users', sa.Column('resume_text', sa.Text(), nullable=True))
|
| 25 |
+
# ### end Alembic commands ###
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def downgrade() -> None:
|
| 29 |
+
"""Downgrade schema."""
|
| 30 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 31 |
+
op.drop_column('users', 'resume_text')
|
| 32 |
+
# ### end Alembic commands ###
|
alembic/versions/3436be625910_create_applications_table.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""create applications table
|
| 2 |
+
|
| 3 |
+
Revision ID: 3436be625910
|
| 4 |
+
Revises: 6f056783c0d8
|
| 5 |
+
Create Date: 2026-06-30 11:27:20.563498
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
from typing import Sequence, Union
|
| 9 |
+
|
| 10 |
+
from alembic import op
|
| 11 |
+
import sqlalchemy as sa
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# revision identifiers, used by Alembic.
|
| 15 |
+
revision: str = '3436be625910'
|
| 16 |
+
down_revision: Union[str, Sequence[str], None] = '6f056783c0d8'
|
| 17 |
+
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
+
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def upgrade() -> None:
|
| 22 |
+
"""Upgrade schema."""
|
| 23 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 24 |
+
op.create_table('applications',
|
| 25 |
+
sa.Column('id', sa.Integer(), nullable=False),
|
| 26 |
+
sa.Column('user_id', sa.Integer(), nullable=False),
|
| 27 |
+
sa.Column('company', sa.String(), nullable=False),
|
| 28 |
+
sa.Column('role', sa.String(), nullable=False),
|
| 29 |
+
sa.Column('status', sa.Enum('applied', 'interview', 'offer', 'rejected', name='applicationstatus'), nullable=False),
|
| 30 |
+
sa.Column('applied_date', sa.Date(), nullable=False),
|
| 31 |
+
sa.Column('jd_text', sa.Text(), nullable=True),
|
| 32 |
+
sa.Column('notes', sa.Text(), nullable=True),
|
| 33 |
+
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
| 34 |
+
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
| 35 |
+
sa.PrimaryKeyConstraint('id')
|
| 36 |
+
)
|
| 37 |
+
# ### end Alembic commands ###
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def downgrade() -> None:
|
| 41 |
+
"""Downgrade schema."""
|
| 42 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 43 |
+
op.drop_table('applications')
|
| 44 |
+
# ### end Alembic commands ###
|
app/cores/config.py
CHANGED
|
@@ -12,7 +12,7 @@ class Settings(BaseSettings):
|
|
| 12 |
SECRET_KEY: str
|
| 13 |
ALGORITHM: str
|
| 14 |
ACCESS_TOKEN_EXPIRE_MINUTES: int
|
| 15 |
-
|
| 16 |
|
| 17 |
model_config = {"env_file": str(env_path), "env_file_encoding": "utf-8"}
|
| 18 |
|
|
|
|
| 12 |
SECRET_KEY: str
|
| 13 |
ALGORITHM: str
|
| 14 |
ACCESS_TOKEN_EXPIRE_MINUTES: int
|
| 15 |
+
GEMINI_API_KEY: str
|
| 16 |
|
| 17 |
model_config = {"env_file": str(env_path), "env_file_encoding": "utf-8"}
|
| 18 |
|
app/cores/database.py
CHANGED
|
@@ -8,7 +8,7 @@ from app.cores.config import settings
|
|
| 8 |
SQLALCHEMY_DATABASE_URL = f"postgresql://{settings.DATABASE_USERNAME}:{settings.DATABASE_PASSWORD}@{settings.DATABASE_HOSTNAME}:{settings.DATABASE_PORT}/{settings.DATABASE_NAME}"
|
| 9 |
|
| 10 |
engine = create_engine(SQLALCHEMY_DATABASE_URL)
|
| 11 |
-
SessionLocal = sessionmaker(autocommit=False,autoflush=False,
|
| 12 |
Base = declarative_base()
|
| 13 |
|
| 14 |
def get_db():
|
|
|
|
| 8 |
SQLALCHEMY_DATABASE_URL = f"postgresql://{settings.DATABASE_USERNAME}:{settings.DATABASE_PASSWORD}@{settings.DATABASE_HOSTNAME}:{settings.DATABASE_PORT}/{settings.DATABASE_NAME}"
|
| 9 |
|
| 10 |
engine = create_engine(SQLALCHEMY_DATABASE_URL)
|
| 11 |
+
SessionLocal = sessionmaker(autocommit=False,autoflush=False,bind=engine)
|
| 12 |
Base = declarative_base()
|
| 13 |
|
| 14 |
def get_db():
|
app/cores/security.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
from datetime import datetime
|
| 2 |
from fastapi.security import OAuth2PasswordBearer
|
| 3 |
from jose import jwt
|
| 4 |
from app.cores.config import settings
|
|
@@ -15,19 +15,19 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
|
|
| 15 |
def hash_password(password: str) -> str:
|
| 16 |
return pwd_context.hash(password)
|
| 17 |
|
| 18 |
-
def verify_password(plain_password: str, hashed_password: str) ->
|
| 19 |
-
return pwd_context.verify(plain_password,hashed_password)
|
| 20 |
|
| 21 |
def create_access_token(data: dict) -> str:
|
| 22 |
to_encode = data.copy()
|
| 23 |
-
expire = datetime.utcnow() +
|
| 24 |
-
to_encode.update
|
| 25 |
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
| 26 |
|
| 27 |
def decode_token(token: str) -> dict:
|
| 28 |
return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
| 29 |
|
| 30 |
-
def get_current_user(token: str = Depends(oauth2_scheme), db: Session= Depends(get_db
|
| 31 |
credentials_exception = HTTPException(
|
| 32 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 33 |
detail="Could not validate credentials",
|
|
@@ -35,14 +35,12 @@ def get_current_user(token: str = Depends(oauth2_scheme), db: Session= Depends(g
|
|
| 35 |
)
|
| 36 |
try:
|
| 37 |
payload = decode_token(token)
|
| 38 |
-
user_id
|
| 39 |
if user_id is None:
|
| 40 |
raise credentials_exception
|
| 41 |
except:
|
| 42 |
raise credentials_exception
|
| 43 |
-
user = db.query(User).filter(
|
| 44 |
if not user:
|
| 45 |
raise credentials_exception
|
| 46 |
-
return user
|
| 47 |
-
|
| 48 |
-
|
|
|
|
| 1 |
+
from datetime import datetime, timedelta
|
| 2 |
from fastapi.security import OAuth2PasswordBearer
|
| 3 |
from jose import jwt
|
| 4 |
from app.cores.config import settings
|
|
|
|
| 15 |
def hash_password(password: str) -> str:
|
| 16 |
return pwd_context.hash(password)
|
| 17 |
|
| 18 |
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
| 19 |
+
return pwd_context.verify(plain_password, hashed_password)
|
| 20 |
|
| 21 |
def create_access_token(data: dict) -> str:
|
| 22 |
to_encode = data.copy()
|
| 23 |
+
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
| 24 |
+
to_encode.update({"exp": expire})
|
| 25 |
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
| 26 |
|
| 27 |
def decode_token(token: str) -> dict:
|
| 28 |
return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
| 29 |
|
| 30 |
+
def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
|
| 31 |
credentials_exception = HTTPException(
|
| 32 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 33 |
detail="Could not validate credentials",
|
|
|
|
| 35 |
)
|
| 36 |
try:
|
| 37 |
payload = decode_token(token)
|
| 38 |
+
user_id = payload.get("user_id")
|
| 39 |
if user_id is None:
|
| 40 |
raise credentials_exception
|
| 41 |
except:
|
| 42 |
raise credentials_exception
|
| 43 |
+
user = db.query(User).filter(User.id == user_id).first()
|
| 44 |
if not user:
|
| 45 |
raise credentials_exception
|
| 46 |
+
return user
|
|
|
|
|
|
app/main.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
| 1 |
from fastapi import FastAPI
|
| 2 |
-
from app.routers import auth, user
|
| 3 |
|
| 4 |
app = FastAPI(title="Job Tracker API")
|
| 5 |
|
| 6 |
app.include_router(auth.router)
|
| 7 |
app.include_router(user.router)
|
|
|
|
|
|
|
| 8 |
@app.get("/")
|
| 9 |
def root():
|
| 10 |
return {"message": "Job Tracker API is running"}
|
|
|
|
| 1 |
from fastapi import FastAPI
|
| 2 |
+
from app.routers import application, auth, user
|
| 3 |
|
| 4 |
app = FastAPI(title="Job Tracker API")
|
| 5 |
|
| 6 |
app.include_router(auth.router)
|
| 7 |
app.include_router(user.router)
|
| 8 |
+
app.include_router(application.router)
|
| 9 |
+
|
| 10 |
@app.get("/")
|
| 11 |
def root():
|
| 12 |
return {"message": "Job Tracker API is running"}
|
app/models/application.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import enum
|
| 2 |
+
from sqlalchemy import Column, Date, DateTime, Enum, ForeignKey, Integer, String, Text, func
|
| 3 |
+
from app.cores.database import Base
|
| 4 |
+
from sqlalchemy.orm import relationship
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class ApplicationStatus(str, enum.Enum):
|
| 8 |
+
applied = "applied"
|
| 9 |
+
interview = "interview"
|
| 10 |
+
offer = "offer"
|
| 11 |
+
rejected = "rejected"
|
| 12 |
+
|
| 13 |
+
class Application(Base):
|
| 14 |
+
__tablename__ = "applications"
|
| 15 |
+
|
| 16 |
+
id = Column(Integer, primary_key=True, nullable=False)
|
| 17 |
+
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
| 18 |
+
company = Column(String, nullable=False)
|
| 19 |
+
role = Column(String, nullable=False)
|
| 20 |
+
status = Column(Enum(ApplicationStatus), nullable=False, default=ApplicationStatus.applied)
|
| 21 |
+
applied_date = Column(Date, nullable=False)
|
| 22 |
+
jd_text = Column(Text, nullable=True)
|
| 23 |
+
notes = Column(Text, nullable=True)
|
| 24 |
+
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
| 25 |
+
|
| 26 |
+
owner = relationship("User", back_populates="applications")
|
app/models/user.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
from sqlalchemy import Column, DateTime, Integer, String
|
| 2 |
from sqlalchemy.sql import func
|
| 3 |
from sqlalchemy.orm import relationship
|
| 4 |
from app.cores.database import Base
|
|
@@ -9,6 +9,7 @@ class User(Base):
|
|
| 9 |
id = Column(Integer, primary_key=True, nullable=False)
|
| 10 |
email = Column(String, unique=True, nullable=False)
|
| 11 |
hashed_password = Column(String, nullable=False)
|
|
|
|
| 12 |
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
| 13 |
|
| 14 |
-
|
|
|
|
| 1 |
+
from sqlalchemy import Column, DateTime, Integer, String, Text
|
| 2 |
from sqlalchemy.sql import func
|
| 3 |
from sqlalchemy.orm import relationship
|
| 4 |
from app.cores.database import Base
|
|
|
|
| 9 |
id = Column(Integer, primary_key=True, nullable=False)
|
| 10 |
email = Column(String, unique=True, nullable=False)
|
| 11 |
hashed_password = Column(String, nullable=False)
|
| 12 |
+
resume_text = Column(Text, nullable=True)
|
| 13 |
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
| 14 |
|
| 15 |
+
applications = relationship("Application", back_populates="owner")
|
app/routers/application.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends, HTTPException, status
|
| 2 |
+
from sqlalchemy.orm import Session
|
| 3 |
+
from typing import List
|
| 4 |
+
from app.services.ai_service import analyze_resume_against_jd
|
| 5 |
+
from app.cores.database import get_db
|
| 6 |
+
from app.cores.security import get_current_user
|
| 7 |
+
from app.models.application import Application
|
| 8 |
+
from app.models.user import User
|
| 9 |
+
from app.schemas.application import ApplicationCreate, ApplicationUpdate, ApplicationOut
|
| 10 |
+
|
| 11 |
+
router = APIRouter(prefix="/applications", tags=["Applications"])
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@router.post("", response_model=ApplicationOut, status_code=status.HTTP_201_CREATED)
|
| 15 |
+
def create_application(
|
| 16 |
+
application: ApplicationCreate,
|
| 17 |
+
db: Session = Depends(get_db),
|
| 18 |
+
current_user: User = Depends(get_current_user)
|
| 19 |
+
):
|
| 20 |
+
new_application = Application(
|
| 21 |
+
user_id=current_user.id,
|
| 22 |
+
**application.model_dump()
|
| 23 |
+
)
|
| 24 |
+
db.add(new_application)
|
| 25 |
+
db.commit()
|
| 26 |
+
db.refresh(new_application)
|
| 27 |
+
return new_application
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@router.get("", response_model=List[ApplicationOut])
|
| 31 |
+
def get_applications(
|
| 32 |
+
db: Session = Depends(get_db),
|
| 33 |
+
current_user: User = Depends(get_current_user)
|
| 34 |
+
):
|
| 35 |
+
return db.query(Application).filter(Application.user_id == current_user.id).all()
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@router.get("/{id}", response_model=ApplicationOut)
|
| 39 |
+
def get_application(
|
| 40 |
+
id: int,
|
| 41 |
+
db: Session = Depends(get_db),
|
| 42 |
+
current_user: User = Depends(get_current_user)
|
| 43 |
+
):
|
| 44 |
+
application = db.query(Application).filter(
|
| 45 |
+
Application.id == id,
|
| 46 |
+
Application.user_id == current_user.id
|
| 47 |
+
).first()
|
| 48 |
+
|
| 49 |
+
if not application:
|
| 50 |
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Application not found")
|
| 51 |
+
|
| 52 |
+
return application
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@router.put("/{id}", response_model=ApplicationOut)
|
| 56 |
+
def update_application(
|
| 57 |
+
id: int,
|
| 58 |
+
updated_application: ApplicationUpdate,
|
| 59 |
+
db: Session = Depends(get_db),
|
| 60 |
+
current_user: User = Depends(get_current_user)
|
| 61 |
+
):
|
| 62 |
+
application_query = db.query(Application).filter(
|
| 63 |
+
Application.id == id,
|
| 64 |
+
Application.user_id == current_user.id
|
| 65 |
+
)
|
| 66 |
+
application = application_query.first()
|
| 67 |
+
|
| 68 |
+
if not application:
|
| 69 |
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Application not found")
|
| 70 |
+
|
| 71 |
+
update_data = updated_application.model_dump(exclude_unset=True)
|
| 72 |
+
application_query.update(update_data, synchronize_session=False)
|
| 73 |
+
db.commit()
|
| 74 |
+
db.refresh(application)
|
| 75 |
+
return application
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT)
|
| 79 |
+
def delete_application(
|
| 80 |
+
id: int,
|
| 81 |
+
db: Session = Depends(get_db),
|
| 82 |
+
current_user: User = Depends(get_current_user)
|
| 83 |
+
):
|
| 84 |
+
application_query = db.query(Application).filter(
|
| 85 |
+
Application.id == id,
|
| 86 |
+
Application.user_id == current_user.id
|
| 87 |
+
)
|
| 88 |
+
application = application_query.first()
|
| 89 |
+
|
| 90 |
+
if not application:
|
| 91 |
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Application not found")
|
| 92 |
+
|
| 93 |
+
application_query.delete(synchronize_session=False)
|
| 94 |
+
db.commit()
|
| 95 |
+
return None
|
| 96 |
+
|
| 97 |
+
@router.post("/{id}/analyze", status_code=status.HTTP_200_OK)
|
| 98 |
+
def analyze_application(
|
| 99 |
+
id: int,
|
| 100 |
+
db: Session = Depends(get_db),
|
| 101 |
+
current_user: User = Depends(get_current_user)
|
| 102 |
+
):
|
| 103 |
+
application = db.query(Application).filter(
|
| 104 |
+
Application.id == id,
|
| 105 |
+
Application.user_id == current_user.id
|
| 106 |
+
).first()
|
| 107 |
+
|
| 108 |
+
if not application:
|
| 109 |
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Application not found")
|
| 110 |
+
|
| 111 |
+
if not application.jd_text:
|
| 112 |
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="No job description set for this application")
|
| 113 |
+
|
| 114 |
+
if not current_user.resume_text:
|
| 115 |
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="No resume uploaded for this user")
|
| 116 |
+
|
| 117 |
+
try:
|
| 118 |
+
result = analyze_resume_against_jd(current_user.resume_text, application.jd_text)
|
| 119 |
+
except ValueError as e:
|
| 120 |
+
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e))
|
| 121 |
+
|
| 122 |
+
return result
|
app/routers/auth.py
CHANGED
|
@@ -9,8 +9,8 @@ from sqlalchemy.orm import Session
|
|
| 9 |
|
| 10 |
router = APIRouter(tags=["Authentication"])
|
| 11 |
|
| 12 |
-
router.post("/login", response_model=Token)
|
| 13 |
-
def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db
|
| 14 |
user = db.query(User).filter(User.email == form_data.username).first()
|
| 15 |
if not user or not verify_password(form_data.password, user.hashed_password):
|
| 16 |
raise HTTPException(status_code=401, detail="Invalid credentials")
|
|
|
|
| 9 |
|
| 10 |
router = APIRouter(tags=["Authentication"])
|
| 11 |
|
| 12 |
+
@router.post("/login", response_model=Token)
|
| 13 |
+
def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
| 14 |
user = db.query(User).filter(User.email == form_data.username).first()
|
| 15 |
if not user or not verify_password(form_data.password, user.hashed_password):
|
| 16 |
raise HTTPException(status_code=401, detail="Invalid credentials")
|
app/routers/user.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
| 1 |
from fastapi import APIRouter, HTTPException, status, Depends
|
| 2 |
from sqlalchemy.orm import Session
|
| 3 |
-
from app.cores.security import hash_password
|
| 4 |
from app.models.user import User
|
| 5 |
-
from app.schemas.user import UserCreate, UserOut
|
| 6 |
from app.cores.database import get_db
|
|
|
|
|
|
|
| 7 |
|
| 8 |
router = APIRouter(
|
| 9 |
prefix="/users",
|
|
@@ -11,7 +13,7 @@ router = APIRouter(
|
|
| 11 |
)
|
| 12 |
|
| 13 |
|
| 14 |
-
@router.post("
|
| 15 |
def create_user(user: UserCreate, db: Session = Depends(get_db)):
|
| 16 |
existing = db.query(User).filter(User.email == user.email).first()
|
| 17 |
|
|
@@ -30,4 +32,59 @@ def create_user(user: UserCreate, db: Session = Depends(get_db)):
|
|
| 30 |
db.commit()
|
| 31 |
db.refresh(new_user)
|
| 32 |
|
| 33 |
-
return new_user
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from fastapi import APIRouter, HTTPException, status, Depends
|
| 2 |
from sqlalchemy.orm import Session
|
| 3 |
+
from app.cores.security import hash_password, verify_password, get_current_user
|
| 4 |
from app.models.user import User
|
| 5 |
+
from app.schemas.user import UserCreate, UserOut, PasswordUpdate
|
| 6 |
from app.cores.database import get_db
|
| 7 |
+
from fastapi import UploadFile, File
|
| 8 |
+
from app.services.resume_service import extract_text_from_pdf
|
| 9 |
|
| 10 |
router = APIRouter(
|
| 11 |
prefix="/users",
|
|
|
|
| 13 |
)
|
| 14 |
|
| 15 |
|
| 16 |
+
@router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED)
|
| 17 |
def create_user(user: UserCreate, db: Session = Depends(get_db)):
|
| 18 |
existing = db.query(User).filter(User.email == user.email).first()
|
| 19 |
|
|
|
|
| 32 |
db.commit()
|
| 33 |
db.refresh(new_user)
|
| 34 |
|
| 35 |
+
return new_user
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@router.get("", response_model=list[UserOut], status_code=status.HTTP_200_OK)
|
| 39 |
+
def get_all_user(db: Session = Depends(get_db)):
|
| 40 |
+
return db.query(User).all()
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@router.put("", status_code=status.HTTP_200_OK)
|
| 44 |
+
def update_password(
|
| 45 |
+
password_data: PasswordUpdate,
|
| 46 |
+
db: Session = Depends(get_db),
|
| 47 |
+
current_user: User = Depends(get_current_user)
|
| 48 |
+
):
|
| 49 |
+
if not verify_password(password_data.old_password, current_user.hashed_password):
|
| 50 |
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Old password is incorrect")
|
| 51 |
+
|
| 52 |
+
current_user.hashed_password = hash_password(password_data.new_password)
|
| 53 |
+
db.commit()
|
| 54 |
+
return {"message": "Password updated successfully"}
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT)
|
| 58 |
+
def delete_user(
|
| 59 |
+
id: int,
|
| 60 |
+
db: Session = Depends(get_db),
|
| 61 |
+
):
|
| 62 |
+
user_query = db.query(User).filter(User.id == id)
|
| 63 |
+
user = user_query.first()
|
| 64 |
+
|
| 65 |
+
if not user:
|
| 66 |
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
| 67 |
+
|
| 68 |
+
user_query.delete(synchronize_session=False)
|
| 69 |
+
db.commit()
|
| 70 |
+
return None
|
| 71 |
+
|
| 72 |
+
@router.post("/resume", status_code=status.HTTP_200_OK)
|
| 73 |
+
def upload_resume(
|
| 74 |
+
file: UploadFile = File(...),
|
| 75 |
+
db: Session = Depends(get_db),
|
| 76 |
+
current_user: User = Depends(get_current_user)
|
| 77 |
+
):
|
| 78 |
+
if file.content_type != "application/pdf":
|
| 79 |
+
raise HTTPException(
|
| 80 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 81 |
+
detail="Only PDF files are allowed"
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
file_bytes = file.file.read()
|
| 85 |
+
resume_text = extract_text_from_pdf(file_bytes)
|
| 86 |
+
|
| 87 |
+
current_user.resume_text = resume_text
|
| 88 |
+
db.commit()
|
| 89 |
+
|
| 90 |
+
return {"message": "Resume uploaded successfully", "characters_extracted": len(resume_text)}
|
app/schemas/application.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
from datetime import date, datetime
|
| 3 |
+
from typing import Optional
|
| 4 |
+
from app.models.application import ApplicationStatus
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class ApplicationCreate(BaseModel):
|
| 8 |
+
company: str
|
| 9 |
+
role: str
|
| 10 |
+
status: ApplicationStatus = ApplicationStatus.applied
|
| 11 |
+
applied_date: date
|
| 12 |
+
jd_text: Optional[str] = None
|
| 13 |
+
notes: Optional[str] = None
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class ApplicationUpdate(BaseModel):
|
| 17 |
+
company: Optional[str] = None
|
| 18 |
+
role: Optional[str] = None
|
| 19 |
+
status: Optional[ApplicationStatus] = None
|
| 20 |
+
applied_date: Optional[date] = None
|
| 21 |
+
jd_text: Optional[str] = None
|
| 22 |
+
notes: Optional[str] = None
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class ApplicationOut(BaseModel):
|
| 26 |
+
id: int
|
| 27 |
+
user_id: int
|
| 28 |
+
company: str
|
| 29 |
+
role: str
|
| 30 |
+
status: ApplicationStatus
|
| 31 |
+
applied_date: date
|
| 32 |
+
jd_text: Optional[str] = None
|
| 33 |
+
notes: Optional[str] = None
|
| 34 |
+
created_at: datetime
|
| 35 |
+
|
| 36 |
+
model_config = {"from_attributes": True}
|
app/schemas/user.py
CHANGED
|
@@ -1,15 +1,20 @@
|
|
| 1 |
-
from datetime import datetime
|
| 2 |
from pydantic import BaseModel, EmailStr
|
|
|
|
| 3 |
|
| 4 |
|
| 5 |
class UserCreate(BaseModel):
|
| 6 |
email: EmailStr
|
| 7 |
password: str
|
| 8 |
|
|
|
|
| 9 |
class UserOut(BaseModel):
|
| 10 |
id: int
|
| 11 |
email: EmailStr
|
| 12 |
created_at: datetime
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from pydantic import BaseModel, EmailStr
|
| 2 |
+
from datetime import datetime
|
| 3 |
|
| 4 |
|
| 5 |
class UserCreate(BaseModel):
|
| 6 |
email: EmailStr
|
| 7 |
password: str
|
| 8 |
|
| 9 |
+
|
| 10 |
class UserOut(BaseModel):
|
| 11 |
id: int
|
| 12 |
email: EmailStr
|
| 13 |
created_at: datetime
|
| 14 |
+
|
| 15 |
+
model_config = {"from_attributes": True}
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class PasswordUpdate(BaseModel):
|
| 19 |
+
old_password: str
|
| 20 |
+
new_password: str
|
app/services/__init__.py
ADDED
|
File without changes
|
app/services/ai_service.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import google.generativeai as genai
|
| 3 |
+
from app.cores.config import settings
|
| 4 |
+
|
| 5 |
+
genai.configure(api_key=settings.GEMINI_API_KEY)
|
| 6 |
+
|
| 7 |
+
SYSTEM_PROMPT = """You are a resume analysis assistant. Compare the candidate's resume text against a job description and respond with ONLY valid JSON (no markdown, no preamble, no code fences) in exactly this shape:
|
| 8 |
+
|
| 9 |
+
{
|
| 10 |
+
"match_score": <integer 0-100>,
|
| 11 |
+
"matching_skills": [<string>, ...],
|
| 12 |
+
"missing_skills": [<string>, ...],
|
| 13 |
+
"summary": "<2-3 sentence summary of fit>",
|
| 14 |
+
"tailored_resume": {
|
| 15 |
+
"professional_summary": "<rewritten 2-3 sentence summary tailored to the JD>",
|
| 16 |
+
"skills": [<string>, ...],
|
| 17 |
+
"experience_bullets": [<string>, ...]
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
model = genai.GenerativeModel(
|
| 23 |
+
model_name="gemini-2.5-flash",
|
| 24 |
+
system_instruction=SYSTEM_PROMPT,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
def analyze_resume_against_jd(resume_text: str, jd_text: str) -> dict:
|
| 28 |
+
user_prompt = f"""RESUME:
|
| 29 |
+
{resume_text}
|
| 30 |
+
|
| 31 |
+
JOB DESCRIPTION:
|
| 32 |
+
{jd_text}
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
response = model.generate_content(
|
| 36 |
+
user_prompt,
|
| 37 |
+
generation_config=genai.types.GenerationConfig(
|
| 38 |
+
temperature=0.3,
|
| 39 |
+
response_mime_type="application/json",
|
| 40 |
+
),
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
raw_content = response.text.strip()
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
return json.loads(raw_content)
|
| 47 |
+
except json.JSONDecodeError:
|
| 48 |
+
raise ValueError("AI returned invalid JSON response")
|
app/services/resume_service.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pypdf import PdfReader
|
| 2 |
+
from fastapi import HTTPException, status
|
| 3 |
+
import io
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def extract_text_from_pdf(file_bytes: bytes) -> str:
|
| 7 |
+
try:
|
| 8 |
+
reader = PdfReader(io.BytesIO(file_bytes))
|
| 9 |
+
text = ""
|
| 10 |
+
for page in reader.pages:
|
| 11 |
+
text += page.extract_text() or ""
|
| 12 |
+
|
| 13 |
+
if not text.strip():
|
| 14 |
+
raise HTTPException(
|
| 15 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 16 |
+
detail="Could not extract text from PDF. The file may be scanned/image-based."
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
return text.strip()
|
| 20 |
+
except HTTPException:
|
| 21 |
+
raise
|
| 22 |
+
except Exception:
|
| 23 |
+
raise HTTPException(
|
| 24 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 25 |
+
detail="Invalid or corrupted PDF file"
|
| 26 |
+
)
|
requirements.txt
CHANGED
|
Binary files a/requirements.txt and b/requirements.txt differ
|
|
|