Skip to content

Commit

Permalink
Tacs 44 map gateway to scheduler (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasanchez authored May 5, 2023
2 parents 0a41942 + e224a57 commit 7f43db3
Show file tree
Hide file tree
Showing 21 changed files with 1,140 additions and 34 deletions.
24 changes: 24 additions & 0 deletions api-gateway/app/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,29 @@ def get_application() -> FastAPI:
"url": "https://mit-license.org/",
}

tags_metadata = [
{
"name": "Actuator",
"description": "Verifies application's liveliness and readiness.",
},
{
"name": "Queries",
"description": "A request for data from the system.",
},
{
"name": "Commands",
"description": "A request to change the state of the system.",
},
{
"name": "Scheduler",
"description": "Manages meeting's schedule workflows.",
},
{
"name": "Auth",
"description": "Manages user's validation workflows.",
}
]

app = FastAPI(
title=settings.PROJECT_NAME,
description=settings.PROJECT_DESCRIPTION,
Expand All @@ -83,6 +106,7 @@ def get_application() -> FastAPI:
lifespan=lifespan,
license_info=license_info,
contact=contact,
openapi_tags=tags_metadata,
)

log.debug("Add application routes.")
Expand Down
56 changes: 56 additions & 0 deletions api-gateway/app/domain/commands/scheduler_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""
Scheduler Commands
"""
import datetime

from pydantic import Field

from app.domain.schemas import CamelCaseModel


class ProposeOption(CamelCaseModel):
"""
Command to propose a meeting option.
"""

date: datetime.date = Field(description="The date of the meeting.", example="2021-01-01")
hour: int = Field(description="The hour of the meeting.", example="12", max=23, min=0)
minute: int = Field(description="The minute of the meeting.", example="30", max=59, min=0)


class ScheduleMeeting(CamelCaseModel):
"""
Command to schedule a meeting.
"""

organizer: str = Field(description="Responsible for the meeting's username.", example="johndoe")
title: str | None = Field(description="The meeting's title.", example="Coffee with John Doe")
description: str | None = Field(description="The meeting's description.",
example="A meeting to discuss the project.")
location: str | None = Field(description="The meeting's location.", example="Floor 3, Cafeteria")
options: list[ProposeOption] = Field(description="A list of options to schedule the meeting.", min_items=1)
guests: set[str] = Field(description="A list of guests to invite to the meeting.",
example=["frank", "jane"], default_factory=set)


class ToggleVoting(CamelCaseModel):
"""
Command to toggle voting on a meeting.
"""
username: str = Field(description="Responsible for the meeting's username.", example="johndoe")
voting: bool = Field(description="Whether voting is enabled", example=True, default=True)


class JoinMeeting(CamelCaseModel):
"""
Command to join a meeting.
"""
username: str = Field(description="Username of who wants to join a meeting", example="johndoe")


class VoteOption(CamelCaseModel):
"""
Command to vote on a meeting option.
"""
username: str = Field(description="Username of who wants to vote on a meeting option", example="johndoe")
option: ProposeOption = Field(description="The option to vote on.")
32 changes: 32 additions & 0 deletions api-gateway/app/domain/events/scheduler_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""
Events from scheduler service.
"""
import datetime

from pydantic import Field

from app.domain.schemas import CamelCaseModel


class OptionVoted(CamelCaseModel):
"""
Event emitted when an option is voted.
"""
date: datetime.datetime = Field(description="A tentative date for a meeting")
votes: list[str] = Field(description="A list of usernames that voted for the option.", example=["johndoe"])


class MeetingScheduled(CamelCaseModel):
"""
Event emitted when a meeting is scheduled.
"""

id: str = Field(description="The meeting's unique identifier.", example="6442ee3291a1304d4c88ffc9")
organizer: str = Field(description="Username of who scheduled the meeting.", example="johndoe")
voting: bool = Field(description="Whether mode voting is enabled.", example=True)
title: str = Field(description="The meeting's title.", example="Sprint Planning")
description: str | None = Field(description="The meeting's description.", example="Planning the next sprint.")
location: str | None = Field(description="The meeting's location.", example="Room 1")
date: datetime.datetime | None = Field(description="The meeting's date.", example="2021-01-01T09:30:00Z")
guests: list[str] = Field(description="A list of guests.", example=["johndoe", "clarasmith"], default_factory=list)
options: list[OptionVoted] = Field(description="A list of options.")
1 change: 1 addition & 0 deletions api-gateway/app/domain/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Config(BaseConfig):
alias_generator = to_camel
allow_population_by_field_name = True
allow_arbitrary_types = True
use_enum_values = True
anystr_strip_whitespace = True


Expand Down
204 changes: 204 additions & 0 deletions api-gateway/app/entrypoints/v1/scheduler_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
"""
Scheduler Service Gateway
"""
from typing import Annotated

from fastapi import APIRouter, Path, Request, Response
from starlette.status import HTTP_200_OK, HTTP_201_CREATED

from app.adapters.http_client import AsyncHttpClient
from app.adapters.network import gateway
from app.dependencies import AsyncHttpClientDependency, ServiceProvider
from app.domain.commands.scheduler_service import JoinMeeting, ScheduleMeeting, ToggleVoting, VoteOption
from app.domain.events.scheduler_service import MeetingScheduled
from app.domain.models import Service
from app.domain.schemas import ResponseModel, ResponseModels
from app.service_layer.gateway import api_v1_url, get_service, verify_scheduling_meeting, verify_status, \
verify_user_existence

router = APIRouter(prefix="/scheduler-service", tags=["Scheduler"])


@router.get("/schedules",
status_code=HTTP_200_OK,
summary="Finds all schedules",
tags=["Queries"]
)
async def query_schedules(
services: ServiceProvider,
client: AsyncHttpClientDependency,
) -> ResponseModels[MeetingScheduled]:
"""
Retrieves schedules from the Database.
"""

service = await get_service(service_name="scheduler", services=services)

response, code = await gateway(service_url=service.base_url, path=f"{api_v1_url}/schedules",
client=client, method="GET")

verify_status(response=response, status_code=code)

return ResponseModels[MeetingScheduled](**response)


@router.get("/schedules/{schedule_id}",
status_code=HTTP_200_OK,
summary="Finds schedule by id",
tags=["Queries"]
)
async def query_schedule_by_id(
schedule_id: Annotated[str, Path(description="The schedule's id.", example="b455f6t63t7")],
services: ServiceProvider,
client: AsyncHttpClientDependency,
) -> ResponseModel[MeetingScheduled]:
"""
Retrieves a specific schedule from the Database.
"""

service = await get_service(service_name="scheduler", services=services)

response, code = await gateway(service_url=service.base_url, path=f"{api_v1_url}/schedules/{schedule_id}",
client=client, method="GET")

verify_status(response=response, status_code=code)

return ResponseModel[MeetingScheduled](**response)


@router.post("/schedules",
status_code=HTTP_201_CREATED,
summary="Creates a schedule",
tags=["Commands"],
)
async def schedule(
command: ScheduleMeeting,
services: ServiceProvider,
client: AsyncHttpClientDependency,
request: Request,
response: Response,
) -> ResponseModel[MeetingScheduled]:
"""
Schedules a meeting.
"""

updated_command = await verify_scheduling_meeting(command=command,
service=await get_service(service_name="auth",
services=services),
client=client)

service_response, status_code = await gateway(
service_url=(await get_service(service_name="scheduler", services=services)).base_url,
path=f"{api_v1_url}/schedules",
client=client,
method="POST",
request_body=updated_command.json()
)

verify_status(response=service_response, status_code=status_code, status_codes=[HTTP_201_CREATED])

response_body = ResponseModel[MeetingScheduled](**service_response)

response.headers["Location"] = f"{request.base_url}api/v1/scheduler-service/schedules/{response_body.data.id}"

return response_body


@router.patch("/schedules/{schedule_id}/voting",
status_code=HTTP_200_OK,
summary="Toggles voting on a schedule",
tags=["Commands"],
)
async def toggle_voting(
schedule_id: Annotated[str, Path(description="The schedule's id.", example="b455f6t63t7")],
command: ToggleVoting,
services: ServiceProvider,
client: AsyncHttpClientDependency,
) -> ResponseModel[MeetingScheduled]:
"""
Toggles voting on a schedule.
"""
service_response, status_code = await gateway(
service_url=(await get_service(service_name="scheduler", services=services)).base_url,
path=f"{api_v1_url}/schedules/{schedule_id}/voting",
client=client,
method="PATCH",
request_body=command.json()
)

verify_status(response=service_response, status_code=status_code, status_codes=[HTTP_200_OK])

return ResponseModel[MeetingScheduled](**service_response)


@router.patch("/schedules/{schedule_id}/relationships/guests",
status_code=HTTP_200_OK,
summary="Adds a guest to a schedule",
tags=["Commands"],
)
async def join_meeting(
schedule_id: Annotated[str, Path(description="The schedule's id.", example="b455f6t63t7")],
command: JoinMeeting,
services: ServiceProvider,
client: AsyncHttpClientDependency,
) -> ResponseModel[MeetingScheduled]:
"""
Allows a valid user to join a meeting.
"""
return await command_with_user_validation(
path=f"{schedule_id}/relationships/guests",
command=command,
services=services,
client=client,
)


@router.patch("/schedules/{schedule_id}/options",
status_code=HTTP_200_OK,
summary="Votes for an option",
tags=["Commands"],
)
async def vote_for_option(
schedule_id: Annotated[str, Path(description="The schedule's id.", example="b455f6t63t7")],
command: VoteOption,
services: ServiceProvider,
client: AsyncHttpClientDependency,
) -> ResponseModel[MeetingScheduled]:
return await command_with_user_validation(
path=f"{schedule_id}/options",
command=command,
services=services,
client=client,
)


########################################################################################################################
# Helper functions
########################################################################################################################

async def command_with_user_validation(
path: str,
command: JoinMeeting | VoteOption,
services: list[Service],
client: AsyncHttpClient,
method: str = "PATCH",
) -> ResponseModel[MeetingScheduled]:
"""
Validates the user and then sends the command to the scheduler service.
"""
await verify_user_existence(username=command.username,
client=client,
service=await get_service(service_name="auth", services=services),
)

service_response, status_code = await gateway(
service_url=(await get_service(service_name="scheduler", services=services)).base_url,
client=client,
path=f"{api_v1_url}/schedules/{path}",
method=method,
request_body=command.json()
)

verify_status(response=service_response, status_code=status_code, status_codes=[HTTP_200_OK])

return ResponseModel[MeetingScheduled](**service_response)
3 changes: 2 additions & 1 deletion api-gateway/app/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from fastapi import APIRouter

from app.entrypoints import actuator
from app.entrypoints.v1 import auth_service
from app.entrypoints.v1 import auth_service, scheduler_service

root_router = APIRouter()
api_router_v1 = APIRouter(prefix="/api/v1")
Expand All @@ -17,3 +17,4 @@

# API Routers
api_router_v1.include_router(auth_service.router)
api_router_v1.include_router(scheduler_service.router)
Loading

0 comments on commit 7f43db3

Please sign in to comment.