Skip to content

Commit

Permalink
add search Modeus API
Browse files Browse the repository at this point in the history
Complete #6 tasks
  • Loading branch information
depocoder committed Sep 26, 2024
1 parent 25438f6 commit 531be41
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 30 deletions.
65 changes: 58 additions & 7 deletions backend/app/controllers/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class ModeusCreds(BaseModel):
password: str


class ModeusSearchEvents(BaseModel):
class ModeusEventsBody(BaseModel):
"""Modeus search events body."""
size: int = Field(examples=[50], default=50)
time_min: datetime.datetime = Field(alias="timeMin", examples=[datetime.datetime.now()])
Expand All @@ -28,6 +28,17 @@ class ModeusSearchEvents(BaseModel):
attendee_person_id: list[str] = Field(alias="attendeePersonId", default="d69c87c8-aece-4f39-b6a2-7b467b968211")


class ModeusPersonSearch(BaseModel):
"""Modeus search events body."""
full_name: str = Field(alias="fullName")


class FullModeusPersonSearch(ModeusPersonSearch):
sort: str = Field(default="+fullName")
size: int = Field(default=10)
page: int = Field(default=0)


class Location(BaseModel):
id: uuid.UUID = Field(alias="eventId")
custom_location: str = Field(alias="customLocation")
Expand Down Expand Up @@ -55,25 +66,27 @@ class Href(BaseModel):
def id(self) -> uuid.UUID:
return uuid.UUID(self.href.replace('/', ''))


class Link(BaseModel):
self: Href
event: Href
person: Href


class Attender(BaseModel):
links: Link = Field(alias="_links")


class Teacher(BaseModel):
class ShortPerson(BaseModel):
id: uuid.UUID
full_name: str = Field(alias="fullName")


class Embedded(BaseModel):
class CalendarEmbedded(BaseModel):
events: list[Event] = Field(alias="events")
locations: list[Location] = Field(alias="event-locations")
attendees: list[Attender] = Field(alias="event-attendees")
teacher: list[Teacher] = Field(alias="persons")
people: list[ShortPerson] = Field(alias="persons")


class FullEvent(Event, Location):
Expand All @@ -83,11 +96,12 @@ class FullEvent(Event, Location):
class ModeusCalendar(BaseModel):
"""Modeus calendar response."""

embedded: Embedded = Field(alias="_embedded")
embedded: CalendarEmbedded = Field(alias="_embedded")

def parse_modeus_response(self) -> list[FullEvent]:
def serialize_modeus_response(self) -> list[FullEvent]:
"""Serialize calendar api response from modeus."""
locations = {location.id: location for location in self.embedded.locations}
teachers = {teacher.id: teacher for teacher in self.embedded.teacher}
teachers = {teacher.id: teacher for teacher in self.embedded.people}
teachers_with_events = {teacher.links.event.id: teacher.links for teacher in self.embedded.attendees}
full_events = []
for event in self.embedded.events:
Expand All @@ -103,3 +117,40 @@ def parse_modeus_response(self) -> list[FullEvent]:
**event.model_dump(by_alias=True), **location.model_dump(by_alias=True),
}))
return full_events


class StudentsSpeciality(BaseModel):
id: uuid.UUID = Field(alias="personId")
flow_code: str = Field(alias="flowCode")
learning_start_date: Optional[datetime.datetime] = Field(alias="learningStartDate")
learning_end_date: Optional[datetime.datetime] = Field(alias="learningEndDate")
specialty_code: str = Field(alias="specialtyCode")
specialty_name: str = Field(alias="specialtyName")
specialty_profile: str = Field(alias="specialtyProfile")


class ExtendedPerson(StudentsSpeciality, ShortPerson):
pass


class PeopleEmbedded(BaseModel):
persons: list[ShortPerson]
students: list[StudentsSpeciality]


class SearchPeople(BaseModel):
embedded: PeopleEmbedded = Field(alias="_embedded")

def serialize_modeus_response(self) -> list[ExtendedPerson]:
"""Serialize search people response."""
speciality_ids = {student.id: student for student in self.embedded.students}
extended_people = []
for person in self.embedded.persons:
try:
teacher_event = speciality_ids[person.id]
except KeyError:
continue
extended_people.append(ExtendedPerson(**{
**teacher_event.model_dump(by_alias=True), **person.model_dump(by_alias=True),
}))
return extended_people
26 changes: 20 additions & 6 deletions backend/app/controllers/modeus.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ def class_name(cls) -> str:
return "Modeus"

@post()
async def get_modeus_cookies(self, item: FromJson[models.ModeusCreds]) -> Response:
async def get_modeus_cookies(self, body: FromJson[models.ModeusCreds]) -> Response:
"""
Auth in Modeus and return cookies.
"""
try:
return self.json(
await modeus.login(item.value.username, item.value.password),
await modeus.login(body.value.username, body.value.password),
)
except (RequestException, ModeusError) as exception:
return self.json({"error": f"can't authenticate {exception}"}, status=400)
Expand All @@ -49,14 +49,14 @@ async def get_modeus_cookies(self, item: FromJson[models.ModeusCreds]) -> Respon
async def get_modeus_events(
self,
auth: FromAuthorizationHeader,
item: FromJson[models.ModeusSearchEvents],
body: FromJson[models.ModeusEventsBody],
) -> Response:
"""
Get events from Modeus.
"""
try:
jwt = auth.value.split()[1]
return self.json(await modeus.get_events(jwt, item.value))
return self.json(await modeus.get_events(jwt, body.value))
except IndexError as exception:
return self.json(
{"error": f"cannot parse authorization header {exception}"},
Expand All @@ -68,7 +68,7 @@ async def get_modeus_events(
@post("/events_blank/")
async def get_modeus_events_blank(
self,
item: FromJson[models.ModeusSearchEvents],
body: FromJson[models.ModeusEventsBody],
) -> Response:
"""
Get events from Modeus when no account.
Expand All @@ -78,4 +78,18 @@ async def get_modeus_events_blank(
except (RequestException, ModeusError) as exception:
return self.json({"error": f"can't authenticate {exception}"}, status=400)
auth = FromAuthorizationHeader(value=f"Bearer {jwt_token}")
return await self.get_modeus_events(item=item, auth=auth)
return await self.get_modeus_events(body=body, auth=auth)

@post("/search_blank/")
async def search_blank(
self,
body: FromJson[models.ModeusPersonSearch],
) -> Response:
"""
Search people from Modeus when no account.
"""
try:
jwt_token = (await modeus.login(settings.modeus_username, settings.modeus_password))['token']
return self.json(await modeus.get_people(jwt_token, body.value))
except (RequestException, ModeusError) as exception:
return self.json({"error": f"can't authenticate {exception}"}, status=400)
51 changes: 34 additions & 17 deletions backend/integration/modeus.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
from bs4 import BeautifulSoup, Tag
from httpx import URL, AsyncClient

from app.controllers.models import ModeusSearchEvents, ModeusCalendar, FullEvent
from app.controllers.models import ModeusEventsBody, ModeusCalendar, FullEvent, ModeusPersonSearch, \
FullModeusPersonSearch, SearchPeople, ExtendedPerson
from integration.exceptions import CannotAuthenticateError, LoginFailedError


_token_re = re.compile(r"id_token=([a-zA-Z0-9\-_.]+)")
_AUTH_URL = "https://auth.modeus.org/oauth2/authorize"

Expand Down Expand Up @@ -79,12 +79,12 @@ async def login(username: str, __password: str, timeout: int = 15) -> dict[str,
CannotAuthenticateError: if something changed in API
"""
async with httpx.AsyncClient(
base_url="https://utmn.modeus.org/",
timeout=timeout,
headers={
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0",
},
follow_redirects=True,
base_url="https://utmn.modeus.org/",
timeout=timeout,
headers={
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0",
},
follow_redirects=True,
) as session:
form = await get_auth_form(session, username, __password)
auth_data = {}
Expand Down Expand Up @@ -115,12 +115,7 @@ def _extract_token_from_url(url: str, match_index: int = 1) -> str | None:
return match[match_index]


async def get_events(
__jwt: str,
body: ModeusSearchEvents,
timeout: int = 15,
) -> list[FullEvent]:
"""Get events for student in modeus"""
async def post_modeus(__jwt: str, body: Any, url_part: str, timeout: int = 15):
session = AsyncClient(
http2=True,
base_url="https://utmn.modeus.org/",
Expand All @@ -129,8 +124,30 @@ async def get_events(
session.headers["Authorization"] = f"Bearer {__jwt}"
session.headers["content-type"] = "application/json"
response = await session.post(
"/schedule-calendar-v2/api/calendar/events/search",
url_part,
content=body.model_dump_json(by_alias=True),
)
modeus_calendar = ModeusCalendar.model_validate_json(response.text)
return modeus_calendar.parse_modeus_response()
response.raise_for_status()
return response.text


async def get_events(
__jwt: str,
body: ModeusEventsBody,

) -> list[FullEvent]:
"""Get events for student in modeus"""
response = await post_modeus(__jwt, body, "/schedule-calendar-v2/api/calendar/events/search")
modeus_calendar = ModeusCalendar.model_validate_json(response)
return modeus_calendar.serialize_modeus_response()


async def get_people(
__jwt: str,
body: ModeusPersonSearch,
) -> list[ExtendedPerson]:
"""Get people from modeus"""
full_body = FullModeusPersonSearch(**(body.model_dump(by_alias=True)))
response = await post_modeus(__jwt, full_body, "/schedule-calendar-v2/api/people/persons/search")
search_people = SearchPeople.model_validate_json(response)
return search_people.serialize_modeus_response()

0 comments on commit 531be41

Please sign in to comment.