Skip to content

Commit

Permalink
List jobs (#19)
Browse files Browse the repository at this point in the history
* Add list jobs with status endpoint and associated tests

* Add check to ensure status param in request is a valid JobStatus member. Also add corresponding test

* Add list class method to JobStatus Enum. More informative error message.

* Documentation and pip audit fix

* Update lock file

* remove unecessary error handling
  • Loading branch information
jewelltaylor authored Apr 19, 2024
1 parent 74851d2 commit 2aff399
Show file tree
Hide file tree
Showing 4 changed files with 353 additions and 80 deletions.
10 changes: 10 additions & 0 deletions florist/api/db/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@


JOB_DATABASE_NAME = "job"
MAX_RECORDS_TO_FETCH = 1000


class JobStatus(Enum):
Expand All @@ -21,6 +22,15 @@ class JobStatus(Enum):
FINISHED_WITH_ERROR = "FINISHED_WITH_ERROR"
FINISHED_SUCCESSFULLY = "FINISHED_SUCCESSFULLY"

@classmethod
def list(cls) -> List[str]:
"""
List all the valid statuses.
:return: (List[str]) a list of valid job statuses.
"""
return [status.value for status in JobStatus]


class ClientInfo(BaseModel):
"""Define the information of an FL client."""
Expand Down
24 changes: 22 additions & 2 deletions florist/api/routes/server/job.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""FastAPI routes for the job."""
from json import JSONDecodeError
from typing import Any, Dict
from typing import Any, Dict, List

from fastapi import APIRouter, Body, HTTPException, Request, status
from fastapi.encoders import jsonable_encoder

from florist.api.db.entities import JOB_DATABASE_NAME, Job
from florist.api.db.entities import JOB_DATABASE_NAME, MAX_RECORDS_TO_FETCH, Job, JobStatus


router = APIRouter()
Expand Down Expand Up @@ -45,3 +45,23 @@ async def new_job(request: Request, job: Job = Body(...)) -> Dict[str, Any]: #
assert isinstance(created_job, dict)

return created_job


@router.get(path="/{status}", response_description="List jobs with the specified status", response_model=List[Job])
async def list_jobs_with_status(status: JobStatus, request: Request) -> List[Dict[str, Any]]:
"""
List jobs with specified status.
Fetches list of Job with max length MAX_RECORDS_TO_FETCH.
:param status: (JobStatus) The status of jobs to query the Job DB for.
:param request: (fastapi.Request) the FastAPI request object.
:return: (List[Dict[str, Any]]) A list where each entry is a dictionary with the attributes
of a Job instance with the specified status.
"""
status = jsonable_encoder(status)

job_db = request.app.database[JOB_DATABASE_NAME]
result = await job_db.find({"status": status}).to_list(MAX_RECORDS_TO_FETCH)
assert isinstance(result, list)
return result
237 changes: 236 additions & 1 deletion florist/tests/integration/api/routes/server/test_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from florist.api.clients.common import Client
from florist.api.db.entities import ClientInfo, Job, JobStatus
from florist.api.routes.server.job import new_job
from florist.api.routes.server.job import list_jobs_with_status, new_job
from florist.api.servers.common import Model
from florist.tests.integration.api.utils import mock_request

Expand Down Expand Up @@ -90,3 +90,238 @@ async def test_new_job_fail_bad_server_info(mock_request) -> None:

assert exception_info.value.status_code == 400
assert "job.server_info could not be parsed into JSON" in exception_info.value.detail

async def test_list_jobs_with_status(mock_request) -> None:
test_job1 = Job(
id="test-id1",
status=JobStatus.NOT_STARTED,
model=Model.MNIST,
server_address="test-server-address1",
server_info="{\"test-server-info\": 123}",
redis_host="test-redis-host1",
redis_port="test-redis-port1",
clients_info=[
ClientInfo(
client=Client.MNIST,
service_address="test-addr-1-1",
data_path="test/data/path-1-1",
redis_host="test-redis-host-1-1",
redis_port="test-redis-port-1-1",
),
ClientInfo(
client=Client.MNIST,
service_address="test-addr-2-1",
data_path="test/data/path-2-1",
redis_host="test-redis-host-2-1",
redis_port="test-redis-port-2-1",
),
]
)

test_job2 = Job(
id="test-id2",
status=JobStatus.IN_PROGRESS,
model=Model.MNIST,
server_address="test-server-address2",
server_info="{\"test-server-info\": 123}",
redis_host="test-redis-host2",
redis_port="test-redis-port2",
clients_info=[
ClientInfo(
client=Client.MNIST,
service_address="test-addr-1-2",
data_path="test/data/path-1-2",
redis_host="test-redis-host-1-2",
redis_port="test-redis-port-1-2",
),
ClientInfo(
client=Client.MNIST,
service_address="test-addr-2-2",
data_path="test/data/path-2-2",
redis_host="test-redis-host-2-2",
redis_port="test-redis-port-2-2",
),
]
)

test_job3 = Job(
id="test-id3",
status=JobStatus.FINISHED_WITH_ERROR,
model=Model.MNIST,
server_address="test-server-address3",
server_info="{\"test-server-info\": 123}",
redis_host="test-redis-host3",
redis_port="test-redis-port3",
clients_info=[
ClientInfo(
client=Client.MNIST,
service_address="test-addr-1-3",
data_path="test/data/path-1-3",
redis_host="test-redis-host-1-3",
redis_port="test-redis-port-1-3",
),
ClientInfo(
client=Client.MNIST,
service_address="test-addr-2-3",
data_path="test/data/path-2-3",
redis_host="test-redis-host-2-3",
redis_port="test-redis-port-2-3",
),
]
)

test_job4 = Job(
id="test-id4",
status=JobStatus.FINISHED_SUCCESSFULLY,
model=Model.MNIST,
server_address="test-server-address4",
server_info="{\"test-server-info\": 123}",
redis_host="test-redis-host4",
redis_port="test-redis-port4",
clients_info=[
ClientInfo(
client=Client.MNIST,
service_address="test-addr-1-4",
data_path="test/data/path-1-4",
redis_host="test-redis-host-1-4",
redis_port="test-redis-port-1-4",
),
ClientInfo(
client=Client.MNIST,
service_address="test-addr-2-4",
data_path="test/data/path-2-4",
redis_host="test-redis-host-2-4",
redis_port="test-redis-port-2-4",
),
]
)
await new_job(mock_request, test_job1)
await new_job(mock_request, test_job2)
await new_job(mock_request, test_job3)
await new_job(mock_request, test_job4)

result_not_started = await list_jobs_with_status(JobStatus.NOT_STARTED, mock_request)
result_in_progress = await list_jobs_with_status(JobStatus.IN_PROGRESS, mock_request)
result_finished_with_error = await list_jobs_with_status(JobStatus.FINISHED_WITH_ERROR, mock_request)
result_finished_successfully = await list_jobs_with_status(JobStatus.FINISHED_SUCCESSFULLY, mock_request)

assert isinstance(result_not_started, list)
assert isinstance(result_in_progress, list)
assert isinstance(result_finished_with_error, list)
assert isinstance(result_finished_successfully, list)

assert result_not_started[0] == {
"_id": test_job1.id,
"status": test_job1.status.value,
"model": test_job1.model.value,
"server_address": "test-server-address1",
"server_info": "{\"test-server-info\": 123}",
"redis_host": test_job1.redis_host,
"redis_port": test_job1.redis_port,
"clients_info": [
{
"_id": ANY,
"client": test_job1.clients_info[0].client.value,
"service_address": test_job1.clients_info[0].service_address,
"data_path": test_job1.clients_info[0].data_path,
"redis_host": test_job1.clients_info[0].redis_host,
"redis_port": test_job1.clients_info[0].redis_port,
}, {
"_id": ANY,
"client": test_job1.clients_info[1].client.value,
"service_address": test_job1.clients_info[1].service_address,
"data_path": test_job1.clients_info[1].data_path,
"redis_host": test_job1.clients_info[1].redis_host,
"redis_port": test_job1.clients_info[1].redis_port,
},
],
}
assert isinstance(result_not_started[0]["clients_info"][0]["_id"], str)
assert isinstance(result_not_started[0]["clients_info"][1]["_id"], str)

assert result_in_progress[0] == {
"_id": test_job2.id,
"status": test_job2.status.value,
"model": test_job2.model.value,
"server_address": "test-server-address2",
"server_info": "{\"test-server-info\": 123}",
"redis_host": test_job2.redis_host,
"redis_port": test_job2.redis_port,
"clients_info": [
{
"_id": ANY,
"client": test_job2.clients_info[0].client.value,
"service_address": test_job2.clients_info[0].service_address,
"data_path": test_job2.clients_info[0].data_path,
"redis_host": test_job2.clients_info[0].redis_host,
"redis_port": test_job2.clients_info[0].redis_port,
}, {
"_id": ANY,
"client": test_job2.clients_info[1].client.value,
"service_address": test_job2.clients_info[1].service_address,
"data_path": test_job2.clients_info[1].data_path,
"redis_host": test_job2.clients_info[1].redis_host,
"redis_port": test_job2.clients_info[1].redis_port,
},
],
}
assert isinstance(result_in_progress[0]["clients_info"][0]["_id"], str)
assert isinstance(result_in_progress[0]["clients_info"][1]["_id"], str)

assert result_finished_with_error[0] == {
"_id": test_job3.id,
"status": test_job3.status.value,
"model": test_job3.model.value,
"server_address": "test-server-address3",
"server_info": "{\"test-server-info\": 123}",
"redis_host": test_job3.redis_host,
"redis_port": test_job3.redis_port,
"clients_info": [
{
"_id": ANY,
"client": test_job3.clients_info[0].client.value,
"service_address": test_job3.clients_info[0].service_address,
"data_path": test_job3.clients_info[0].data_path,
"redis_host": test_job3.clients_info[0].redis_host,
"redis_port": test_job3.clients_info[0].redis_port,
}, {
"_id": ANY,
"client": test_job3.clients_info[1].client.value,
"service_address": test_job3.clients_info[1].service_address,
"data_path": test_job3.clients_info[1].data_path,
"redis_host": test_job3.clients_info[1].redis_host,
"redis_port": test_job3.clients_info[1].redis_port,
},
],
}
assert isinstance(result_finished_with_error[0]["clients_info"][0]["_id"], str)
assert isinstance(result_finished_with_error[0]["clients_info"][1]["_id"], str)

assert result_finished_successfully[0] == {
"_id": test_job4.id,
"status": test_job4.status.value,
"model": test_job4.model.value,
"server_address": "test-server-address4",
"server_info": "{\"test-server-info\": 123}",
"redis_host": test_job4.redis_host,
"redis_port": test_job4.redis_port,
"clients_info": [
{
"_id": ANY,
"client": test_job4.clients_info[0].client.value,
"service_address": test_job4.clients_info[0].service_address,
"data_path": test_job4.clients_info[0].data_path,
"redis_host": test_job4.clients_info[0].redis_host,
"redis_port": test_job4.clients_info[0].redis_port,
}, {
"_id": ANY,
"client": test_job4.clients_info[1].client.value,
"service_address": test_job4.clients_info[1].service_address,
"data_path": test_job4.clients_info[1].data_path,
"redis_host": test_job4.clients_info[1].redis_host,
"redis_port": test_job4.clients_info[1].redis_port,
},
],
}
assert isinstance(result_finished_successfully[0]["clients_info"][0]["_id"], str)
assert isinstance(result_finished_successfully[0]["clients_info"][1]["_id"], str)
Loading

0 comments on commit 2aff399

Please sign in to comment.