Skip to content
Merged

Main #40

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
eff67b1
feat: Add Python scripts for database seeding and deletion.
Saannddy Mar 2, 2026
4fdd21a
feat: add Java restroom problems, riddles, and questions, and update …
Saannddy Mar 2, 2026
4da1c04
docs: clarify Docker rebuild conditions and add `DATABASE_URL` export…
Saannddy Mar 2, 2026
0ff2bac
chore: remove Bruno API test files for problem endpoints and initiali…
Saannddy Mar 2, 2026
fe76a2b
chore: update coverage folder to allow to run in both local python an…
Saannddy Mar 3, 2026
7991383
Merge pull request #37 from Saannddy/update-pytest-coverage-for-unittest
Saannddy Mar 3, 2026
151baf0
chore: Standardize coverage report directory name, update test execut…
Saannddy Mar 3, 2026
bfcf3a4
chore: remove non necessary space
Saannddy Mar 3, 2026
d67f5ad
feat: Add Python scripts for database seeding and deletion.
Saannddy Mar 2, 2026
05599d9
feat: add Java restroom problems, riddles, and questions, and update …
Saannddy Mar 2, 2026
0e6b065
docs: clarify Docker rebuild conditions and add `DATABASE_URL` export…
Saannddy Mar 2, 2026
66f7e97
chore: remove Bruno API test files for problem endpoints and initiali…
Saannddy Mar 2, 2026
df6aca5
chore: Standardize coverage report directory name, update test execut…
Saannddy Mar 3, 2026
89b715e
chore: remove non necessary space
Saannddy Mar 3, 2026
aceba30
Merge branch 'implement-seeder-for-tag-riddles-and-question' of https…
Saannddy Mar 3, 2026
55b4481
feat: Move `seed_restroom_java.py` into a new `seeders` subdirectory …
Saannddy Mar 3, 2026
089b58f
Merge pull request #36 from Saannddy/implement-seeder-for-tag-riddles…
Saannddy Mar 3, 2026
92343df
feat: Include question choices when fetching random questions by tag.
Saannddy Mar 3, 2026
1fd8cac
Merge branch 'dev' of https://github.com/Saannddy/CodeExecutor-API in…
Saannddy Mar 3, 2026
cef2dde
feat: add fina all non hidden choice by quesiton id
Saannddy Mar 3, 2026
03b7060
feat: hot fix on get random question
Saannddy Mar 3, 2026
d25b330
feat: Eagerly load question choices, tags, and categories to prevent …
Saannddy Mar 3, 2026
446a539
chore: refactor response format of chunk to be in more likely the sam…
Saannddy Mar 4, 2026
4be7336
scripts: update seeding riddles data and seeder script
Saannddy Mar 4, 2026
39dbb6c
docs: Improve README readability by adding blank lines, updating a co…
Saannddy Mar 4, 2026
0e7d800
feat: Implement chunk service, add dynamic import placeholders to chu…
Saannddy Mar 7, 2026
96f5beb
refactor: remove 'indent' prefix from validation and logic placeholde…
Saannddy Mar 7, 2026
2abde95
feat: Replace handlebar patterns with language-specific TODO comments…
Saannddy Mar 7, 2026
9375fb7
Add riddles and seeding scripts for Java hallway and locker room
Saannddy Mar 11, 2026
187b553
feat: Add problems and test cases for Java restroom challenges, updat…
Saannddy Mar 11, 2026
ba41cd4
Merge pull request #38 from Saannddy/update-seeder
Saannddy Mar 11, 2026
01165c6
Merge pull request #39 from Saannddy/dev
Saannddy Mar 11, 2026
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
2 changes: 1 addition & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ exclude_lines =
raise NotImplementedError

[html]
directory = coverage_html
directory = coverage
title = CodeExecutor-API Coverage Report
show_contexts = true
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ alembic/versions/__pycache__/
*.exe

# Testing / Coverage
coverage_html/
coverage/
.coverage
.pytest_cache/
htmlcov/
14 changes: 14 additions & 0 deletions API_DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,28 @@
Dynamic code execution API supporting multiple languages and database-backed problem management.

## Base URL

`http://localhost:3000`

## Interactive Documentation

Interactive docs are available at: [http://localhost:3000/docs](http://localhost:3000/docs)

---

## Endpoints

### 1. List Problems

Retrieve a summary of all problems in the database. Supports filtering by category or tag.

- **URL**: `/problems`
- **Method**: `GET`
- **Query Parameters**:
- `category`: Filter by category name (e.g., `/problems?category=Math`)
- `tag`: Filter by tag name (e.g., `/problems?tag=String`)
- **Response**: `200 OK`

```json
{
"status": "success",
Expand All @@ -30,16 +35,21 @@ Retrieve a summary of all problems in the database. Supports filtering by catego
```

### 2. Get Problem Details

Retrieve specific problem metadata, configuration, and public test cases.

- **URL**: `/problem/<problem_id>`
- **Method**: `GET`
- **Response**: `200 OK`

### 3. Execute Code (Problem-based)

Run code against test cases associated with a specific problem.

- **URL**: `/code/<problem_id>`
- **Method**: `POST`
- **Body**:

```json
{
"language": "python",
Expand All @@ -48,10 +58,13 @@ Run code against test cases associated with a specific problem.
```

### 4. Custom Execution

Run arbitrary code without a pre-defined problem.

- **URL**: `/run?lang=python`
- **Method**: `POST`
- **Body**:

```json
{
"code": "print('hello world')"
Expand All @@ -61,6 +74,7 @@ Run arbitrary code without a pre-defined problem.
---

## Supported Languages

- **Python**: `python`
- **JavaScript**: `javascript`
- **Java**: `java`
Expand Down
97 changes: 81 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ If you modify the models in `src/models/`, use these commands to sync the databa
- **Check Status**: `docker compose --profile local exec code-api alembic current`
- **Apply Migrations**: `docker compose --profile local exec code-api alembic upgrade head`
- **Generate New Migration**:

```bash
docker compose --profile local exec code-api alembic revision --autogenerate -m "description_of_change"
```
Expand All @@ -85,23 +86,63 @@ If you want to connect to **NeonDB** online and migrate it to the latest version
1. **Get Connection String**: Copy your connection string from the [Neon Console](https://console.neon.tech/) (ensure `?sslmode=require` is included).
2. **Apply Migrations**:
Run the following command, replacing `YOUR_NEON_URL` with your actual connection string:

```bash
docker compose --profile local exec -e DATABASE_URL="YOUR_NEON_URL" local-code-api alembic upgrade head
```

3. **(Optional) Seed Data**:
If your Neon database is empty, you can seed it with the default problems:

```bash
docker compose --profile local exec -e DATABASE_URL="YOUR_NEON_URL" local-code-api python3 -m scripts.seed
```

_(Alternatively, you can simply update the `DATABASE_URL` in your `.env` file and run the standard migration commands.)_

### 🌱 Data Seeding
### 🌱 Data Seeding & Cleanup

For manual data management, use these commands from the **project root**:

#### 1️⃣ Java Restroom Seeding (40+ Questions & Riddles)

If you need to re-seed or reset the initial data:
Seed the database with Java MCQs, riddles, and problems tagged as `JAV_RESTROOM`:

```bash
# Running via Docker (Must rebuild if scripts are modified)
docker compose --profile local up -d --build
docker compose --profile local exec local-code-api python3 -m scripts.seeders.seed_restroom_java

# Running Locally (Ensure .env is configured or set DATABASE_URL)
export DATABASE_URL=postgresql://postgres:postgres@localhost:5432/code_executor
PYTHONPATH=src python3 src/scripts/seeders/seed_restroom_java.py
```

#### 2️⃣ Clear All Database Data

Delete all entries from all tables (Riddles, Questions, Problems, etc.):

```bash
# Running via Docker
docker compose --profile local exec local-code-api python3 -m scripts.delete

# Running Locally
export DATABASE_URL=postgresql://postgres:postgres@localhost:5432/code_executor
PYTHONPATH=src python3 src/scripts/delete.py
```

#### 3️⃣ Standard Sample Seeding

Seed the original set of coding problems (Two Sum, etc.):

```bash
# Running via Docker (Recommended)
docker compose --profile local exec local-code-api python3 -m scripts.seed

- **Run Seeder**: `docker compose --profile local exec local-code-api python3 -m scripts.seed`
_(The seeder is idempotent and will skip problems that already exist!)_
# Running Locally
export DATABASE_URL=postgresql://postgres:postgres@localhost:5432/code_executor
PYTHONPATH=src python3 src/scripts/seed.py
```

---

Expand Down Expand Up @@ -149,7 +190,7 @@ For the best developer experience, we've included a **Bruno Collection**:

Tests are organized by feature group in the `tests/` directory:

```
```cmd
tests/
├── conftest.py # Shared fixtures (Flask client, DB mocks)
├── problem/ # Problem endpoint tests
Expand All @@ -160,30 +201,54 @@ tests/
└── docs/ # Documentation endpoint tests
```

### Run All Tests
### Run with Coverage Report

```bash
python3 -m pytest
```
The tests automatically generate an HTML report inside the container. To view it locally:

### Coverage Report
```bash
# 1. Run the tests
docker compose --profile local exec local-code-api python3 -m pytest

After running tests, open the interactive HTML coverage report:
# 2. Copy the report from the container to your machine
docker cp codeexecutor-api-local-code-api-1:/usr/src/app/coverage/ ./coverage/

```bash
open coverage_html/index.html
# 3. Open it
open coverage/index.html
```

The report highlights **exactly which lines** were hit or missed during testing — click any file to see line-by-line coverage.

### Run Locally (Outside Docker)

If you want to run tests without Docker for faster iteration:

1. **Install Dependencies**:

```bash
pip install Flask gunicorn pytest sqlmodel alembic python-dotenv pydantic-settings esprima javalang pytest-cov pybars3
```

_(Note: `psycopg2-binary` might fail on some Python versions, but unit tests are mocked so you can skip it.)_

2. **Run Tests**:

```bash
PYTHONPATH=src python3 -m pytest
```

3. **View Coverage**:

```bash
open coverage/index.html
```

### Run a Specific Test Group

```bash
python3 -m pytest tests/problem/ # Problem tests only
python3 -m pytest tests/riddle/ # Riddle tests only
PYTHONPATH=src python3 -m pytest tests/problem/
```

> **Note**: Tests mock the database layer so they run locally without Postgres. Install test dependencies first: `pip install pytest pytest-cov`
> **Note**: Tests mock the database layer automatically, so no local Postgres is required.

---

Expand Down
4 changes: 2 additions & 2 deletions bruno/local/Question/get all random questions.bru
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ meta {
}

get {
url: http://localhost:3000/question/random?tag=trivia
url: http://localhost:3000/question/random?tag=JAV_RESTROOM
body: none
auth: inherit
}

params:query {
tag: trivia
tag: JAV_RESTROOM
}

settings {
Expand Down
2 changes: 1 addition & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ addopts =
--cov=services
--cov=repositories
--cov=api
--cov-report=html:coverage_html
--cov-report=html:coverage
--cov-config=.coveragerc
10 changes: 5 additions & 5 deletions src/handlers/chunk_handler.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
from flask import jsonify, request
from repositories.chunk_repository import ChunkRepository
from services.chunk_service import ChunkService

class ChunkHandler:
def __init__(self):
self.repo = ChunkRepository()
self.service = ChunkService()

def get_all_chunks(self):
page = request.args.get('page', default=1, type=int)
limit = request.args.get('limit', default=10, type=int)
lang = request.args.get('lang')

chunks = self.repo.find_all(page=page, limit=limit, lang=lang)
chunks = self.service.get_all_chunks(page=page, limit=limit, lang=lang)
return jsonify(status="success", data=chunks), 200

def get_chunk(self, chunk_id):
lang = request.args.get('lang')
chunk = self.repo.get_details(chunk_id, lang=lang)
chunk = self.service.get_chunk(chunk_id, lang=lang)
if not chunk:
return jsonify(status="error", message="Chunk not found"), 404
return jsonify(status="success", data=chunk), 200
Expand All @@ -24,5 +24,5 @@ def get_random_chunks(self):
limit = request.args.get('limit', default=1, type=int)
lang = request.args.get('lang')

chunks = self.repo.find_random(limit=limit, lang=lang)
chunks = self.service.get_random_chunks(limit=limit, lang=lang)
return jsonify(status="success", data=chunks), 200
6 changes: 6 additions & 0 deletions src/repositories/choice_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,9 @@ def count_by_question_id(self, question_id: UUID):
with self._get_session() as session:
statement = select(Choice).where(Choice.question_id == question_id)
return len(session.exec(statement).all())

def find_all_by_question_id(self, question_id: UUID):
"""Fetch all choices that is not hidden for a question."""
with self._get_session() as session:
statement = select(Choice).where(Choice.question_id == question_id)
return session.exec(statement).all()
40 changes: 23 additions & 17 deletions src/repositories/chunk_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,31 +25,40 @@ def find_all(self, page=1, limit=10, lang=None):

statement = statement.order_by(Chunk.id).offset((page - 1) * limit).limit(limit)

# Use unique() for joinedload with collections
results = session.exec(statement).unique().all()
chunks = []
for chunk in results:
c_dict = self._serialize_chunk(chunk, lang)
chunks.append(c_dict)
chunks.append(self._serialize_chunk(chunk, lang))
return chunks

def _serialize_chunk(self, chunk, lang=None):
"""Helper to serialize a chunk and optionally filter its templates by language."""
c_dict = chunk.model_dump()
c_dict["templates"] = []

# Move templates to config key in response
c_dict["config"] = {"templates": {}}

for t in chunk.templates:
# Skip if language filter is active and doesn't match
if lang and t.language != lang:
continue

t_dict = {
"id": str(t.id),
"language": t.language,
"name": t.name,
"template_code": t.template_code,
"description": t.description,
"snippets": [{"placeholder_key": s.placeholder_key, "code_content": s.code_content} for s in t.snippets]
"snippets": {s.placeholder_key: s.code_content for s in t.snippets}
}
c_dict["templates"].append(t_dict)
c_dict["config"]["templates"][t.language] = t_dict

# Add expectations to config for consistency
if hasattr(chunk, "expectations") and chunk.expectations:
c_dict["expectations"] = [
{"input": e.input, "output": e.output} for e in chunk.expectations
]

return c_dict

def find_by_id(self, chunk_id):
Expand All @@ -72,22 +81,19 @@ def get_details(self, chunk_id, lang=None):
def find_random(self, limit=1, lang=None):
"""Fetch random N chunks with their implementation details. Filters by language if provided."""
with self._get_session() as session:
statement = select(Chunk).options(
joinedload(Chunk.templates).joinedload(ChunkTemplate.snippets)
)

if lang:
# Require that the chunk HAS a template in that language
statement = statement.join(Chunk.templates).where(ChunkTemplate.language == lang)

statement = statement.order_by(func.random()).limit(limit)

results = session.exec(statement).unique().all()
statement = select(Chunk).order_by(func.random())
# For simplicity, if lang is provided, we fetch more and filter in Python
# or just fetch all and filter.
results = session.exec(statement).all()
if not results:
return []

chunks = []
for chunk in results:
if lang and lang not in chunk.config.get("templates", {}):
continue
chunks.append(self._serialize_chunk(chunk, lang))
if len(chunks) >= limit:
break

return chunks
Loading
Loading