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
5 changes: 2 additions & 3 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
name: Python application

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

Expand All @@ -26,7 +24,8 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
pip install -r requirements.txt
pip install pytest pytest-mock
- name: Test with pytest
run: |
pytest -v --junitxml=pytest.xml -o junit_logging=all .
Expand Down
5 changes: 1 addition & 4 deletions application/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,16 @@ async def predict(self, prediction_request: PredictionRequest):
job_id = str(uuid4())
self.jobs[job_id] = {"status": "processing", "result": None}
asyncio.create_task(self.process_job(job_id, prediction_request))
# await asyncio.sleep(random.random() * 3) # Simulate a long-running task
# response = await prediction_job

return {"job_id": job_id}

async def process_job(self, job_id: str, member_features: PredictionRequest):
try:
await asyncio.sleep(random.random() * 3) # Simulate a long-running task

result = await get_predictions(member_features)
self.jobs[job_id]["status"] = "completed"
self.jobs[job_id]["result"] = result

return result
except Exception as e:
self.jobs[job_id]["status"] = "failed"
self.jobs[job_id]["result"] = str(e)
Expand Down
18 changes: 0 additions & 18 deletions francis.md

This file was deleted.

3 changes: 3 additions & 0 deletions machine_learning/predict.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Dict
from models.prediction_request import PredictionRequest
import random
import asyncio


@staticmethod
Expand All @@ -28,6 +29,8 @@ async def get_predictions(
else:
probability_to_transact = 0.0

await asyncio.sleep(random.random() * 3) # Simulate a long-running task

return {
"average_transaction_size": avg_transaction_size,
"probability_to_transact": probability_to_transact
Expand Down
42 changes: 42 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Docker test execution

docker pull python:3.11-slim

docker run -it -w /var/plus -v $(pwd):/var/plus python:3.11-slim bash -c "pip install -r requirements.txt && pip install pytest pytest-mock && pytest -v"

# Local execution

Setup:
pip install -r requirements.txt
pip install pytest pytest-mock

Execution:
pytest

# Docker Web API Execution

To simply start an instance of the Web API

docker pull python:3.11-slim

docker run -it -p 8000:8000 -w /var/plus -v $(pwd):/var/plus python:3.11-slim bash -c "pip install -r requirements.txt && fastapi dev main.py --host 0.0.0.0"

# Code changes from original application code
- Fix requirements for fastapi : fastapi[standard]
- Refactor POST - /predict to not wait for a response and return the job_id :
- Remove return typing
- Change code to not make the job function call back predict function (infinite loop)
- Move the "await asyncio.sleep(random.random() * 3)" to the predict function.

# Work done

## Tests
- Basic tests for each endpoint - 200
- Check for basic invalid parameters (Json payload non good, bad or missing path ID)
- Check prediction result status based on the execution state
- Use mocking to prevent call to the prediction system, to prevent dependencies on unstable system and to remove waiting for the execution to complete.
- Also use mocking to prevent the execution of prediction system to control if a job is completed or pending.

## GitHub
- Setup a Git Hub action to execute test when a PR is created.
- Also add basic reporting of the test in the Git Hub Action job summary
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,3 @@ typing-inspection==0.4.0
typing_extensions==4.13.2
uuid==1.30
uvicorn==0.34.2
pytest==8.4.1
57 changes: 49 additions & 8 deletions test/test_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
from httpx import ASGITransport, AsyncClient
from main import app
import pytest
import anyio
from unittest.mock import AsyncMock


client = TestClient(app)

Expand All @@ -12,6 +13,37 @@ def anyio_backend():
return 'asyncio'


@pytest.fixture()
def mock_get_prediction(mocker):
async_mock = AsyncMock()
mocker.patch('application.app.get_predictions', side_effect=async_mock)

async_mock.return_value = {
"average_transaction_size": 50.0,
"probability_to_transact": 0.75
}

return async_mock


@pytest.fixture()
def mock_get_prediction_in_error(mocker):
async_mock = AsyncMock()
mocker.patch('application.app.get_predictions', side_effect=async_mock)

async_mock.side_effect = Exception("Something happened")

return async_mock


@pytest.fixture()
def mock_process_jop_do_nothing(mocker):
async_mock = AsyncMock()
mocker.patch('application.app.Application.process_job', side_effect=async_mock)

return async_mock


@pytest.fixture()
async def async_client():
async with AsyncClient(
Expand All @@ -37,37 +69,46 @@ async def job_id(async_client):


@pytest.mark.anyio
async def test_result_200_success(async_client, job_id):
async def test_result_200_success(mock_get_prediction, async_client, job_id):

await anyio.sleep(3)
response = await async_client.get(f"/result/{job_id}")

assert response.status_code == 200

assert response.json() == {
"job_id": job_id,
"result": {
"average_transaction_size": 505.0,
"probability_to_transact": 0.4602739726027397
"average_transaction_size": 50.0,
"probability_to_transact": 0.75
}
}


@pytest.mark.anyio
async def test_status_400_processing(async_client, job_id):
async def test_status_400_processing(mock_process_jop_do_nothing, async_client, job_id):

response = await async_client.get(f"/result/{job_id}")

assert response.status_code == 400

assert response.json() == {'detail': 'Result not ready'}
assert response.json() == {"detail": "Result not ready"}


@pytest.mark.anyio
async def test_result_404(async_client):
async def test_result_404(mock_process_jop_do_nothing, async_client):

response = await async_client.get("/result/bad_job_id")

assert response.status_code == 404

assert response.json() == {"detail": "Job ID not found"}


@pytest.mark.anyio
async def test_status_500_fail(mock_get_prediction_in_error, async_client, job_id):

response = await async_client.get(f"/result/{job_id}")

assert response.status_code == 500

assert response.json() == {"detail": "Unknown error occurred during prediction"}
40 changes: 35 additions & 5 deletions test/test_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from httpx import ASGITransport, AsyncClient
from main import app
import pytest
import anyio
from unittest.mock import AsyncMock

client = TestClient(app)

Expand All @@ -12,6 +12,27 @@ def anyio_backend():
return 'asyncio'


@pytest.fixture()
def mock_get_prediction(mocker):
async_mock = AsyncMock()
mocker.patch('application.app.get_predictions', side_effect=async_mock)

async_mock.return_value = {
"average_transaction_size": 50.0,
"probability_to_transact": 0.75
}

return async_mock


@pytest.fixture()
def mock_process_jop_do_nothing(mocker):
async_mock = AsyncMock()
mocker.patch('application.app.Application.process_job', side_effect=async_mock)

return async_mock


@pytest.fixture()
async def async_client():
async with AsyncClient(
Expand All @@ -37,9 +58,8 @@ async def job_id(async_client):


@pytest.mark.anyio
async def test_status_200_success(async_client, job_id):
async def test_status_200_success(mock_get_prediction, async_client, job_id):

await anyio.sleep(3)
response = await async_client.get(f"/status/{job_id}")

assert response.status_code == 200
Expand All @@ -51,7 +71,7 @@ async def test_status_200_success(async_client, job_id):


@pytest.mark.anyio
async def test_status_200_processing(async_client, job_id):
async def test_status_200_processing(mock_process_jop_do_nothing, async_client, job_id):

response = await async_client.get(f"/status/{job_id}")

Expand All @@ -64,10 +84,20 @@ async def test_status_200_processing(async_client, job_id):


@pytest.mark.anyio
async def test_status_404(async_client):
async def test_status_404_bad_id(async_client):

response = await async_client.get("/status/bad_job_id")

assert response.status_code == 404

assert response.json() == {"detail": "Job ID not found"}


@pytest.mark.anyio
async def test_status_404_missing(async_client):

response = await async_client.get("/status/")

assert response.status_code == 404

assert response.json() == {"detail": "Not Found"}