Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .coverage
Binary file not shown.
2 changes: 2 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from fastapi import FastAPI
from app.modules.jobs.routes import router as jobs_router
from app.modules.stats.routes import router as stats_router

app = FastAPI(title = "Job Search API",
description="CS 3321 Project - Job Search API",
version="1.0.0")

# Include the jobs router
app.include_router(stats_router, prefix="/stats", tags=["stats"])
app.include_router(jobs_router, prefix="/jobs", tags=["jobs"])

@app.get("/")
Expand Down
Empty file removed app/modules/__init__.py
Empty file.
2 changes: 1 addition & 1 deletion app/modules/jobs/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ async def get_jobs(q: str = ""):
Skeleton route for fetching jobs.
Later, this will call the Adzuna Job Search API."""

return {"message": f"Yay! Jobs endpoint is working", "query": q}
return {"message": f"Yay! Jobs endpoint is working", "query": q}
38 changes: 38 additions & 0 deletions app/modules/stats/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from fastapi import APIRouter
from .service import calculate_job_stats

router = APIRouter(
tags=["stats"]
)

@router.get("/")
async def get_job_stats(q: str = ""):
"""
Computes statistics for a given job search query.
"""
mock_adzuna_data = {
"count": 3,
"results": [
{
"title": "Python Backend Engineer",
"company": {"display_name": "TechFlow"},
"location": {"display_name": "Remote"},
"description": "This is a fully remote position."
},
{
"title": "Data Scientist",
"company": {"display_name": "Health Analytics"},
"location": {"display_name": "Austin, TX"},
"description": "On-site role in Texas."
},
{
"title": "Junior Python Developer",
"company": {"display_name": "TechFlow"},
"location": {"display_name": "Remote"},
"description": "Work from anywhere."
}
]
}

stats = calculate_job_stats(mock_adzuna_data)
return stats
37 changes: 37 additions & 0 deletions app/modules/stats/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from collections import Counter

def calculate_job_stats(adzuna_data):
# Extract the list of jobs from the JSON dictionary
jobs_list = adzuna_data.get("results", [])

# 1. Calculate Total Jobs
total_jobs = len(jobs_list)

# 2. Calculate Remote Jobs
remote_jobs = 0
for job in jobs_list:
location_name = job.get("location", {}).get("display_name", "").lower()
description = job.get("description", "").lower()

# Check if the job mentions remote work
if "remote" in location_name or "remote" in description:
remote_jobs += 1

# 3. Calculate Top Companies
company_names = []
for job in jobs_list:
# Grab the company name, default to "Unknown" if it's missing
name = job.get("company", {}).get("display_name", "Unknown")
company_names.append(name)

# Use Counter to find the most frequently occurring companies
company_tally = Counter(company_names)
# Grab the top 3 most common companies
top_companies = [company[0] for company in company_tally.most_common(3)]

# Return the final formatted statistics
return {
"Total_jobs": total_jobs,
"Remote_jobs": remote_jobs,
"top_companies": top_companies
}
51 changes: 51 additions & 0 deletions app/tests/test_stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from app.modules.stats.service import calculate_job_stats

def test_calculate_job_stats():
# 1. SETUP: Create a controlled sample of data
mock_data = {
"count": 3,
"results": [
{
"title": "Python Backend Engineer",
"company": {"display_name": "TechFlow"},
"location": {"display_name": "Remote"},
"description": "This is a fully remote position."
},
{
"title": "Data Scientist",
"company": {"display_name": "Health Analytics"},
"location": {"display_name": "Austin, TX"},
"description": "On-site role in Texas."
},
{
"title": "Junior Python Developer",
"company": {"display_name": "TechFlow"},
"location": {"display_name": "Remote"},
"description": "Work from anywhere."
}
]
}

# 2. EXECUTE: Pass the fake data into your function
result = calculate_job_stats(mock_data)

# 3. ASSERT: Verify the function returns the exact numbers it should
assert result["Total_jobs"] == 3
assert result["Remote_jobs"] == 2
assert result["top_companies"] == ["TechFlow", "Health Analytics"]


def test_calculate_job_stats_empty_response():
# 1. SETUP: Simulate Adzuna finding zero jobs
mock_empty_data = {
"count": 0,
"results": []
}

# 2. EXECUTE: Pass the empty data into your function
result = calculate_job_stats(mock_empty_data)

# 3. ASSERT: Verify the math gracefully handles the zeros
assert result["Total_jobs"] == 0
assert result["Remote_jobs"] == 0
assert result["top_companies"] == []
18 changes: 18 additions & 0 deletions app/tests/test_stats_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from fastapi.testclient import TestClient
from app.main import app # Imports your actual FastAPI application

# Create a fake web browser to test your app
client = TestClient(app)

def test_stats_endpoint_success():
# 1. ACT: Send a fake GET request to the stats route
response = client.get("/stats")

# 2. ASSERT: Check that the server responded with a "200 OK" success code
assert response.status_code == 200

# 3. ASSERT: Check that the JSON body sent to the browser is correct
data = response.json()
assert data["Total_jobs"] == 3
assert data["Remote_jobs"] == 2
assert "TechFlow" in data["top_companies"]
14 changes: 10 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
[project]
name = "job-search-api"
version = "0.1.0"
description = "CS 3321 Project - Job Search API"
dependencies = [
"fastapi"
"fastapi",
"httpx>=0.28.1",
"pytest-cov>=7.1.0",
"uvicorn",
]

[dependency-groups]
Expand All @@ -10,6 +16,7 @@ dev = [
]

[tool.pytest.ini_options]
pythonpath = ["."]
testpaths = ["app/tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
Expand Down Expand Up @@ -38,7 +45,6 @@ exclude_lines = [
"raise NotImplementedError",
"if __name__ == \"__main__\":",
"if TYPE_CHECKING:",
"@abstractmethod",
"@abc.abstractmethod",
" @abstractmethod",
" @abc.abstractmethod",
]

Loading