abdullah090809 commited on
Commit
cf25e9f
·
1 Parent(s): 2d2110c

Backend i guess complete

Browse files
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
- GROQ_API_KEY: str
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,blind=engine)
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) -> 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() + datetime.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,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: int = TokenData(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
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
- application = relationship("Applications", back_populates="owner")
 
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("/", response_model=UserOut, status_code=status.HTTP_201_CREATED)
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
- model_config = {
14
- "from_attribute": True
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