From 294a871dff3f31489ef28edc50d198d880c4b9c6 Mon Sep 17 00:00:00 2001 From: existentialcoder Date: Fri, 31 Oct 2025 17:57:55 +0100 Subject: [PATCH] feat: Setup complete FastAPI backend skeleton --- apps/backend/.gitignore | 4 +- apps/backend/Dockerfile | 0 apps/backend/README.md | 0 apps/backend/main.py | 57 ------------------- apps/backend/requirements.txt | 3 +- apps/backend/run.sh | 2 +- apps/backend/src/api/deps/__init__.py | 0 apps/backend/src/api/deps/db.py | 8 +++ apps/backend/src/api/deps/pagination.py | 50 +++++++++++++++++ apps/backend/src/api/v1/routes/__init__.py | 0 apps/backend/src/api/v1/routes/companies.py | 0 apps/backend/src/api/v1/routes/jobs.py | 39 +++++++++++++ apps/backend/src/api/v1/routes/plugins.py | 0 apps/backend/src/api/v1/routes/users.py | 0 apps/backend/src/controller.py | 58 ------------------- apps/backend/src/core/__init__.py | 0 apps/backend/src/core/config.py | 14 +++++ apps/backend/src/core/constants.py | 3 + apps/backend/src/core/logging.py | 0 apps/backend/src/database.py | 21 ------- apps/backend/src/db/base_class.py | 9 +++ apps/backend/src/db/session.py | 17 ++++++ apps/backend/src/main.py | 18 ++++++ apps/backend/src/models.py | 36 ------------ apps/backend/src/models/company.py | 20 +++++++ apps/backend/src/models/job.py | 39 +++++++++++++ apps/backend/src/models/skill.py | 11 ++++ apps/backend/src/models/user.py | 13 +++++ apps/backend/src/schemas.py | 40 ------------- apps/backend/src/schemas/company.py | 34 +++++++++++ apps/backend/src/schemas/job.py | 62 +++++++++++++++++++++ apps/backend/src/schemas/skill.py | 14 +++++ apps/backend/src/schemas/user.py | 12 ++++ apps/backend/src/services/job.py | 18 ++++++ 34 files changed, 386 insertions(+), 216 deletions(-) create mode 100644 apps/backend/Dockerfile create mode 100644 apps/backend/README.md delete mode 100644 apps/backend/main.py create mode 100644 apps/backend/src/api/deps/__init__.py create mode 100644 apps/backend/src/api/deps/db.py create mode 100644 apps/backend/src/api/deps/pagination.py create mode 100644 apps/backend/src/api/v1/routes/__init__.py create mode 100644 apps/backend/src/api/v1/routes/companies.py create mode 100644 apps/backend/src/api/v1/routes/jobs.py create mode 100644 apps/backend/src/api/v1/routes/plugins.py create mode 100644 apps/backend/src/api/v1/routes/users.py delete mode 100644 apps/backend/src/controller.py create mode 100644 apps/backend/src/core/__init__.py create mode 100644 apps/backend/src/core/config.py create mode 100644 apps/backend/src/core/constants.py create mode 100644 apps/backend/src/core/logging.py delete mode 100644 apps/backend/src/database.py create mode 100644 apps/backend/src/db/base_class.py create mode 100644 apps/backend/src/db/session.py create mode 100644 apps/backend/src/main.py delete mode 100644 apps/backend/src/models.py create mode 100644 apps/backend/src/models/company.py create mode 100644 apps/backend/src/models/job.py create mode 100644 apps/backend/src/models/skill.py create mode 100644 apps/backend/src/models/user.py delete mode 100644 apps/backend/src/schemas.py create mode 100644 apps/backend/src/schemas/company.py create mode 100644 apps/backend/src/schemas/job.py create mode 100644 apps/backend/src/schemas/skill.py create mode 100644 apps/backend/src/schemas/user.py create mode 100644 apps/backend/src/services/job.py diff --git a/apps/backend/.gitignore b/apps/backend/.gitignore index 65fb03b..ddce285 100644 --- a/apps/backend/.gitignore +++ b/apps/backend/.gitignore @@ -1,5 +1,5 @@ -.jobs-api-venv/ - +*env/ +*venv/ # Byte-compiled / optimized / DLL files __pycache__/ *.py[codz] diff --git a/apps/backend/Dockerfile b/apps/backend/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/apps/backend/README.md b/apps/backend/README.md new file mode 100644 index 0000000..e69de29 diff --git a/apps/backend/main.py b/apps/backend/main.py deleted file mode 100644 index 0ebe744..0000000 --- a/apps/backend/main.py +++ /dev/null @@ -1,57 +0,0 @@ -import os -from fastapi import FastAPI, Depends, HTTPException -from fastapi.middleware.cors import CORSMiddleware -from sqlalchemy.orm import Session -from uuid import UUID - -from src import models, schemas, controller, database - -# Create tables -models.Base.metadata.create_all(bind=database.engine) - -app = FastAPI(title='Job Applica API') - -app.add_middleware( - CORSMiddleware, - allow_origins=['*'], - allow_methods=['*'], - allow_headers=['*'], -) - -# --- Dependencies --- -def get_db(): - db = database.SessionLocal() - try: - yield db - finally: - db.close() - -# --- Job Endpoints --- -@app.post('/jobs', response_model=schemas.Job) -def create_job(job: schemas.JobBase, db: Session = Depends(get_db)): - return controller.create_job(db, job) - -@app.get('/jobs/{job_id}', response_model=schemas.Job) -def get_job(job_id: UUID, db: Session = Depends(get_db)): - db_job = controller.get_job(db, job_id) - if not db_job: - raise HTTPException(status_code=404, detail='Job not found') - return db_job - -@app.get('/jobs', response_model=list[schemas.Job]) -def get_jobs(skip: int = 0, limit: int = 100, job_title: str | None = None, db: Session = Depends(get_db)): - return controller.get_jobs(db, skip, limit, job_title) - -@app.put('/jobs/{job_id}', response_model=schemas.Job) -def update_job(job_id: UUID, job_update: schemas.JobBase, db: Session = Depends(get_db)): - db_job = controller.update_job(db, job_id, job_update) - if not db_job: - raise HTTPException(status_code=404, detail='Job not found') - return db_job - -@app.delete('/jobs/{job_id}') -def delete_job(job_id: UUID, db: Session = Depends(get_db)): - success = controller.delete_job(db, job_id) - if not success: - raise HTTPException(status_code=404, detail='Job not found') - return {'detail': 'Job deleted'} diff --git a/apps/backend/requirements.txt b/apps/backend/requirements.txt index 93be21e..a9889c3 100644 --- a/apps/backend/requirements.txt +++ b/apps/backend/requirements.txt @@ -2,8 +2,9 @@ fastapi uvicorn[standard] SQLAlchemy psycopg2-binary -pydantic +alembic pydantic[email] +pydantic-settings passlib[bcrypt] python-dotenv requests==2.32.1 diff --git a/apps/backend/run.sh b/apps/backend/run.sh index 539bcc2..a3ac65c 100644 --- a/apps/backend/run.sh +++ b/apps/backend/run.sh @@ -1 +1 @@ -uvicorn main:app --reload +uvicorn src.main:app --reload diff --git a/apps/backend/src/api/deps/__init__.py b/apps/backend/src/api/deps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/backend/src/api/deps/db.py b/apps/backend/src/api/deps/db.py new file mode 100644 index 0000000..9b2e6a3 --- /dev/null +++ b/apps/backend/src/api/deps/db.py @@ -0,0 +1,8 @@ +from ...db.session import SessionLocal + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/apps/backend/src/api/deps/pagination.py b/apps/backend/src/api/deps/pagination.py new file mode 100644 index 0000000..b5b37f5 --- /dev/null +++ b/apps/backend/src/api/deps/pagination.py @@ -0,0 +1,50 @@ +from fastapi import Query, HTTPException +from math import ceil +from typing import Optional +from ...core.constants import Constants + + +async def pagination_params( + page: Optional[int] = Query(1, ge=1, description='Page number (1-indexed)'), + per_page: Optional[int] = Query( + Constants.DEFAULT_PAGE_SIZE, ge=1, le=Constants.MAX_PAGE_SIZE, description='Items per page' + ), +): + ''' + Common dependency to extract pagination parameters from query string. + Example: ?page=2&per_page=20 + ''' + return {'page': page, 'per_page': per_page} + + +def paginate_query(query, pagination: dict): + ''' + Apply pagination to an SQLAlchemy query using page & per_page. + ''' + page = pagination.get('page', 1) + per_page = pagination.get('per_page', Constants.DEFAULT_PAGE_SIZE) + + if per_page > Constants.MAX_PER_PAGE: + raise HTTPException(status_code=400, detail=f'per_page cannot exceed {Constants.MAX_PER_PAGE}') + + offset = (page - 1) * per_page + return query.limit(per_page).offset(offset) + + +def build_paginated_response(items: list, total: int, page: int, per_page: int): + ''' + Build a consistent paginated response payload with metadata. + ''' + total_pages = ceil(total / per_page) if total > 0 else 1 + + return { + 'meta': { + 'total': total, + 'page': page, + 'per_page': per_page, + 'total_pages': total_pages, + 'has_next': page < total_pages, + 'has_prev': page > 1, + }, + 'results': items, + } diff --git a/apps/backend/src/api/v1/routes/__init__.py b/apps/backend/src/api/v1/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/backend/src/api/v1/routes/companies.py b/apps/backend/src/api/v1/routes/companies.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/backend/src/api/v1/routes/jobs.py b/apps/backend/src/api/v1/routes/jobs.py new file mode 100644 index 0000000..6f1c002 --- /dev/null +++ b/apps/backend/src/api/v1/routes/jobs.py @@ -0,0 +1,39 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from uuid import UUID + +from ....schemas import job as schemas +from ...deps.db import get_db +from ...deps.pagination import pagination_params +from ....services import job as job_service + +router = APIRouter(prefix='/jobs') + +# @router.post('/', response_model=schemas.JobBase) +# def create_job(job_in: schemas.JobBase, db: Session = Depends(get_db)): +# return job_service.create_job(db, job_in) + +# @router.get('/{job_id}', response_model=schemas.JobBase) +# def get_job(job_id: UUID, db: Session = Depends(get_db)): +# db_job = job_service.get_job(db, job_id) +# if not db_job: +# raise HTTPException(status_code=404, detail='Job not found') +# return db_job + +@router.get('/', response_model=list[schemas.JobBase]) +def list_jobs(query: str | None = None, pagination: dict = Depends(pagination_params), db: Session = Depends(get_db)): + return job_service.get_jobs(db, pagination, query) + +# @router.put('/{job_id}', response_model=schemas.JobBase) +# def update_job(job_id: UUID, job_update: schemas.JobBase, db: Session = Depends(get_db)): +# db_job = job_service.update_job(db, job_id, job_update) +# if not db_job: +# raise HTTPException(status_code=404, detail='Job not found') +# return db_job + +# @router.delete('/{job_id}')bs +# def delete_job(job_id: UUID, db: Session = Depends(get_db)): +# success = job_service.delete_job(db, job_id) +# if not success: +# raise HTTPException(status_code=404, detail='Job not found') +# return {'detail': 'Job deleted'} diff --git a/apps/backend/src/api/v1/routes/plugins.py b/apps/backend/src/api/v1/routes/plugins.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/backend/src/api/v1/routes/users.py b/apps/backend/src/api/v1/routes/users.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/backend/src/controller.py b/apps/backend/src/controller.py deleted file mode 100644 index 2dd8abf..0000000 --- a/apps/backend/src/controller.py +++ /dev/null @@ -1,58 +0,0 @@ -from sqlalchemy.orm import Session -from .models import Job -from .schemas import JobBase -from passlib.context import CryptContext -from uuid import UUID - -# pwd_context = CryptContext(schemes=['bcrypt'], deprecated="auto") - - -# def create_user(db: Session, user: UserCreate) -> User: -# hashed_password = pwd_context.hash(user.password) -# db_user = User(**user.dict(exclude={"password"}), password=hashed_password) -# db.add(db_user) -# db.commit() -# db.refresh(db_user) -# return db_user - -# def get_user(db: Session, user_id: UUID) -> User | None: -# return db.query(User).filter(User.user_id == user_id).first() - -# def get_users(db: Session, skip: int = 0, limit: int = 100): -# return db.query(User).offset(skip).limit(limit).all() - - -# --- JOB CRUD --- -def create_job(db: Session, job: JobBase) -> Job: - db_job = Job(**job.dict()) - db.add(db_job) - db.commit() - db.refresh(db_job) - return db_job - -def get_job(db: Session, job_id: UUID) -> Job | None: - return db.query(Job).filter(Job.job_id == job_id).first() - -def get_jobs(db: Session, skip: int, limit: int, job_title: str | None = None): - query = db.query(Job) - if job_title: - query = query.filter(Job.job_title.ilike(f"%{job_title}%")) - return query.offset(skip).limit(limit).all() - -def update_job(db: Session, job_id: UUID, job_update: JobBase) -> Job | None: - db_job = db.query(Job).filter(Job.job_id == job_id).first() - if not db_job: - return None - for key, value in job_update.dict(exclude_unset=True).items(): - setattr(db_job, key, value) - db.commit() - db.refresh(db_job) - return db_job - -def delete_job(db: Session, job_id: UUID) -> bool: - db_job = db.query(Job).filter(Job.job_id == job_id).first() - if not db_job: - return False - db.delete(db_job) - db.commit() - return True diff --git a/apps/backend/src/core/__init__.py b/apps/backend/src/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/backend/src/core/config.py b/apps/backend/src/core/config.py new file mode 100644 index 0000000..c4d1c1f --- /dev/null +++ b/apps/backend/src/core/config.py @@ -0,0 +1,14 @@ +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + # Database + DATABASE_URL: str + BACKEND_CORS_ORIGINS: list[str] = [] + LOG_LEVEL: str = 'INFO' + + class Config: + env_file = '.env' + case_sensitive = True + + +settings = Settings() diff --git a/apps/backend/src/core/constants.py b/apps/backend/src/core/constants.py new file mode 100644 index 0000000..be5a86f --- /dev/null +++ b/apps/backend/src/core/constants.py @@ -0,0 +1,3 @@ +class Constants: + DEFAULT_PAGE_SIZE: int = 10 + MAX_PAGE_SIZE: int = 100 diff --git a/apps/backend/src/core/logging.py b/apps/backend/src/core/logging.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/backend/src/database.py b/apps/backend/src/database.py deleted file mode 100644 index 329f9dc..0000000 --- a/apps/backend/src/database.py +++ /dev/null @@ -1,21 +0,0 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker -import os -from dotenv import load_dotenv - -load_dotenv() - -DATABASE_URL = os.getenv( - "DATABASE_URL", - "postgresql+psycopg2://user:password@localhost:5432/jobsdb" -) - -engine = create_engine(DATABASE_URL) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() diff --git a/apps/backend/src/db/base_class.py b/apps/backend/src/db/base_class.py new file mode 100644 index 0000000..9ff83dd --- /dev/null +++ b/apps/backend/src/db/base_class.py @@ -0,0 +1,9 @@ +from datetime import datetime +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column +from sqlalchemy import DateTime + +class Base(DeclarativeBase): + id: Mapped[int] = mapped_column('id', primary_key=True, autoincrement=True) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) + updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + __abstract__ = True diff --git a/apps/backend/src/db/session.py b/apps/backend/src/db/session.py new file mode 100644 index 0000000..ea32ac3 --- /dev/null +++ b/apps/backend/src/db/session.py @@ -0,0 +1,17 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from ..core.config import settings + +DATABASE_URL = settings.DATABASE_URL + +engine = create_engine( + DATABASE_URL, + echo=settings.LOG_LEVEL == 'debug', + future=True +) + +SessionLocal = sessionmaker( + autocommit=False, + autoflush=False, + bind=engine +) diff --git a/apps/backend/src/main.py b/apps/backend/src/main.py new file mode 100644 index 0000000..9b4739b --- /dev/null +++ b/apps/backend/src/main.py @@ -0,0 +1,18 @@ +from fastapi import FastAPI +from .api.v1.routes import jobs +from .models import company, job, skill, user +from .db.base_class import Base +from .db.session import engine + +# Ensure tables are created +Base.metadata.create_all(bind=engine) + +app = FastAPI(title='JobApplica API') + +api_v1_prefix = '/api/v1' + +app.include_router(jobs.router, prefix=api_v1_prefix, tags=['Jobs']) +# app.include_router(users.router, prefix=api_v1_prefix, tags=['Users']) +# app.include_router(plugins.router, prefix=api_v1_prefix, tags=['Plugins']) +# app.include_router(auth.router, prefix=api_v1_prefix, tags=['Auth']) +# app.include_router(health.router, prefix=api_v1_prefix, tags=['Health']) diff --git a/apps/backend/src/models.py b/apps/backend/src/models.py deleted file mode 100644 index e80d926..0000000 --- a/apps/backend/src/models.py +++ /dev/null @@ -1,36 +0,0 @@ -from sqlalchemy import Column, String, Integer, Enum, ForeignKey, ARRAY -from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.orm import relationship -from sqlalchemy.ext.declarative import declarative_base -import uuid -# from enums import JobStatus - -Base = declarative_base() - -# class User(Base): -# __tablename__ = 'users' - -# user_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) -# first_name = Column(String, nullable=False) -# last_name = Column(String, nullable=False) -# email = Column(String, unique=True, nullable=False) -# password = Column(String, nullable=False) -# skills = Column(ARRAY(String)) - -# jobs = relationship('Job', back_populates='user') - - -class Job(Base): - __tablename__ = 'jobs' - - job_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - job_title = Column(String, nullable=False) - company = Column(String, nullable=False) - location = Column(String, nullable=False) - status = Column(String, nullable=False) - category = Column(String, nullable=False) - salary_range = Column(String) - required_skills = Column(ARRAY(String)) - job_description = Column(String) - min_years_of_experience = Column(Integer) - max_years_of_experience = Column(Integer) diff --git a/apps/backend/src/models/company.py b/apps/backend/src/models/company.py new file mode 100644 index 0000000..1cf657e --- /dev/null +++ b/apps/backend/src/models/company.py @@ -0,0 +1,20 @@ +# src/models/company.py +from sqlalchemy import String, Integer +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from ..db.base_class import Base + +class Company(Base): + __tablename__ = 'companies' + name: Mapped[str] = mapped_column(String(100), nullable=False, unique=True) + website: Mapped[str] = mapped_column(String(255), nullable=True) + email: Mapped[str] = mapped_column(String(255), nullable=True) + location: Mapped[str] = mapped_column(String(255), nullable=False) + size: Mapped[int] = mapped_column(Integer, nullable=True) + headquarters: Mapped[str] = mapped_column(String(255), nullable=True) + founded_year: Mapped[int] = mapped_column(Integer, nullable=True) + industry: Mapped[str] = mapped_column(String(255), nullable=True) + description: Mapped[str] = mapped_column(String(500), nullable=True) + + def __repr__(self): + return f"" diff --git a/apps/backend/src/models/job.py b/apps/backend/src/models/job.py new file mode 100644 index 0000000..9f578c0 --- /dev/null +++ b/apps/backend/src/models/job.py @@ -0,0 +1,39 @@ +# src/models/job.py +from sqlalchemy import String, Integer, ForeignKey, JSON +from sqlalchemy.orm import Mapped, mapped_column, relationship +from ..db.base_class import Base +from .company import Company +from .skill import Skill + +from sqlalchemy import Table, Column + +job_skill_table = Table( + 'job_skill', + Base.metadata, + Column('job_id', Integer, ForeignKey('jobs.id', ondelete='CASCADE'), primary_key=True), + Column('skill_id', Integer, ForeignKey('skills.id', ondelete='CASCADE'), primary_key=True), +) + +class Job(Base): + __tablename__ = 'jobs' + job_title: Mapped[str] = mapped_column(String(255), nullable=False) + + status: Mapped[str] = mapped_column(String(50), nullable=False) + position: Mapped[str] = mapped_column(String(50), nullable=False) + category: Mapped[str] = mapped_column(String(100), nullable=True) + salary_range: Mapped[str] = mapped_column(String(100), nullable=True) + job_description: Mapped[str] = mapped_column(String(500), nullable=True) + + years_of_experience: Mapped[dict] = mapped_column(JSON, nullable=True) + + company: Mapped['Company'] = relationship('Company', back_populates='jobs') + company_id: Mapped[int] = mapped_column(ForeignKey('companies.id'), nullable=True) + + required_skills: Mapped[list['Skill']] = relationship( + 'Skill', + secondary=job_skill_table, + back_populates='jobs', + ) + + def __repr__(self): + return f'' diff --git a/apps/backend/src/models/skill.py b/apps/backend/src/models/skill.py new file mode 100644 index 0000000..1dd4c48 --- /dev/null +++ b/apps/backend/src/models/skill.py @@ -0,0 +1,11 @@ +from sqlalchemy import String, Integer +from sqlalchemy.orm import Mapped, mapped_column +from ..db.base_class import Base + +class Skill(Base): + __tablename__ = 'skills' + name: Mapped[str] = mapped_column(String(255), nullable=False) + proficiency_level: Mapped[int] = mapped_column(Integer, nullable=False) + + def __repr__(self) -> str: + return f'' diff --git a/apps/backend/src/models/user.py b/apps/backend/src/models/user.py new file mode 100644 index 0000000..11d8691 --- /dev/null +++ b/apps/backend/src/models/user.py @@ -0,0 +1,13 @@ +from sqlalchemy import String, Integer +from sqlalchemy.orm import Mapped, mapped_column +from ..db.base_class import Base + +class User(Base): + __tablename__ = 'users' + first_name: Mapped[str] = mapped_column(String(100), nullable=False) + last_name: Mapped[str] = mapped_column(String(100), nullable=False) + user_name: Mapped[str | None] = mapped_column(String(100), nullable=True) + email: Mapped[str | None] = mapped_column(String(255), nullable=True) + signup_key: Mapped[str] = mapped_column(String(255), nullable=False) + def __repr__(self) -> str: + return f'' diff --git a/apps/backend/src/schemas.py b/apps/backend/src/schemas.py deleted file mode 100644 index 4018daf..0000000 --- a/apps/backend/src/schemas.py +++ /dev/null @@ -1,40 +0,0 @@ -from typing import List, Optional -from uuid import UUID -from pydantic import BaseModel, EmailStr - -# User schemas -# class UserBase(BaseModel): -# first_name: str -# last_name: str -# email: EmailStr -# skills: List[str] = [] - -# class UserCreate(UserBase): -# password: str - -# class User(UserBase): -# user_id: UUID - -# class Config: -# from_attributes = True - - -# Job schemas -class JobBase(BaseModel): - job_title: str = '' - company: str = '' - location: str = '' - status: str = '' - category: str = '' - salary_range: str = '' - required_skills: List[str] = [] - job_description: Optional[str] = "" - min_years_of_experience: int = 0 - max_years_of_experience: int = 0 - - -class Job(JobBase): - job_id: UUID - - class Config: - from_attributes = True diff --git a/apps/backend/src/schemas/company.py b/apps/backend/src/schemas/company.py new file mode 100644 index 0000000..4da57b4 --- /dev/null +++ b/apps/backend/src/schemas/company.py @@ -0,0 +1,34 @@ +from pydantic import BaseModel, Field, EmailStr, HttpUrl +from uuid import UUID +from datetime import datetime +from typing import Optional + +class CompanyBase(BaseModel): + name: str = Field(..., max_length=100) + website: HttpUrl | None = None + email: EmailStr | None = None + location: str + size: int | None = None + headquarters: str | None = None + founded_year: int | None = None + website: str | None = None + industry: str | None = None + description: str | None = None + + + +class CompanyCreate(CompanyBase): + pass + + +class CompanyUpdate(CompanyBase): + pass + + +class Company(CompanyBase): + id: int + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True diff --git a/apps/backend/src/schemas/job.py b/apps/backend/src/schemas/job.py new file mode 100644 index 0000000..943eafd --- /dev/null +++ b/apps/backend/src/schemas/job.py @@ -0,0 +1,62 @@ +from enum import Enum +from pydantic import BaseModel, Field +from typing import Optional, List +from datetime import date + +from .company import Company +from .skill import SkillBase + + +class JobStatus(str, Enum): + OPEN = 'Open' + CLOSED = 'Closed' + PENDING = 'Pending' + + +class JobPosition(str, Enum): + INTERN = 'Intern' + JUNIOR = 'Junior' + MID = 'Mid' + SENIOR = 'Senior' + LEAD = 'Lead' + MANAGER = 'Manager' + + +class YearsOfExperience(BaseModel): + min: Optional[int] = Field(None, ge=0, description='Minimum years of experience') + max: Optional[int] = Field(None, ge=0, description='Maximum years of experience') + + +class JobBase(BaseModel): + title: str + company: Optional[Company] = None + status: JobStatus = JobStatus.OPEN + position: JobPosition + category: Optional[str] = None + salary_range: Optional[str] = None + required_skills: List[SkillBase] = [] + description: str = '' + years_of_experience: Optional[YearsOfExperience] = None + + model_config = {'from_attributes': True} + + +class JobRead(JobBase): + id: int + created_at: Optional[date] = None + updated_at: Optional[date] = None + + +class JobCreate(JobBase): + pass + + +class JobUpdate(BaseModel): + job_title: Optional[str] = None + status: Optional[JobStatus] = None + position: Optional[JobPosition] = None + category: Optional[str] = None + salary_range: Optional[str] = None + required_skills: Optional[List[SkillBase]] = None + job_description: Optional[str] = None + years_of_experience: Optional[YearsOfExperience] = None diff --git a/apps/backend/src/schemas/skill.py b/apps/backend/src/schemas/skill.py new file mode 100644 index 0000000..fa3c67f --- /dev/null +++ b/apps/backend/src/schemas/skill.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel, Field +from typing import Optional + +class SkillBase(BaseModel): + name: str + proficiency_level: int = Field(..., ge=1, le=10) + + +class SkillCreate(SkillBase): + pass + +class Skill(SkillBase): + class Config: + from_attributes = True diff --git a/apps/backend/src/schemas/user.py b/apps/backend/src/schemas/user.py new file mode 100644 index 0000000..c4b8b4e --- /dev/null +++ b/apps/backend/src/schemas/user.py @@ -0,0 +1,12 @@ +from pydantic import BaseModel, Field, EmailStr + +class UserSignupKey(str): + USER_NAME = 'username' + EMAIL = 'email' + +class UserBase(BaseModel): + first_name: str + last_name: str + user_name: str | None = None + email: EmailStr | None = None + signup_key: UserSignupKey diff --git a/apps/backend/src/services/job.py b/apps/backend/src/services/job.py new file mode 100644 index 0000000..9e4b1f4 --- /dev/null +++ b/apps/backend/src/services/job.py @@ -0,0 +1,18 @@ +import sqlalchemy +from sqlalchemy.orm import Session +from ..models.job import Job +from ..schemas.job import JobBase +from ..api.deps.pagination import paginate_query + +# allowed_job_query_fields = [JobBase.title, JobBase.description, JobBase.company] + +def get_jobs(db: Session, pagination: dict, query: str | None = None) -> list[JobBase]: + # query maybe like this query="company: Acme Inc,location: Remote" + q = db.query(Job) + + # if query: + # query_filters = [] + # for field in allowed_job_query_fields: + # query_filters.append(getattr(JobBase, field).ilike(f'%{query}%')) + # q = q.filter(sqlalchemy.or_(*query_filters)) + return paginate_query(q, pagination)