Skip to content

Commit

Permalink
fixup! ✨(api) retrieve experience from its IRI
Browse files Browse the repository at this point in the history
  • Loading branch information
lebaudantoine committed Jan 13, 2024
1 parent f198a55 commit 826c473
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 78 deletions.
55 changes: 51 additions & 4 deletions src/api/core/warren/tests/xi/client/test_crud_experience.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from warren.xi.client import CRUDExperience
from warren.xi.factories import ExperienceFactory
from warren.xi.models import ExperienceCreate, ExperienceRead
from warren.xi.models import ExperienceCreate, ExperienceRead, ExperienceReadSnapshot


@pytest.mark.anyio
Expand Down Expand Up @@ -62,6 +62,52 @@ async def test_crud_experience_get_not_found(
assert response is None


@pytest.mark.anyio
async def test_crud_experience_get_from_iri_non_existing_experience(
http_client: AsyncClient, db_session: Session
):
"""Test 'get_from_iri' for a non-existing experience."""
crud_instance = CRUDExperience(client=http_client)

# Simulate not found experience by mocking the 'read' method
crud_instance.read = AsyncMock(return_value=[])
crud_instance.get = AsyncMock()

# Get random experience data
data = ExperienceFactory.build_dict()

# Attempt getting a non-existing experience
res = await crud_instance.get_from_iri(data["iri"])

assert res is None

crud_instance.read.assert_awaited_once()
crud_instance.get.assert_not_awaited()


@pytest.mark.anyio
async def test_crud_experience_get_from_iri_existing_experience(
http_client: AsyncClient, db_session: Session
):
"""Test 'get_from_iri' for an existing experience."""
crud_instance = CRUDExperience(client=http_client)

# Get random experience data
data = ExperienceFactory.build_dict(exclude={})

# Simulate not found experience by mocking the 'read' method
crud_instance.read = AsyncMock(return_value=[ExperienceReadSnapshot(**data)])
crud_instance.get = AsyncMock(return_value=ExperienceRead(**data))

# Attempt getting a non-existing experience
res = await crud_instance.get_from_iri(data["iri"])

assert isinstance(res, ExperienceRead)

crud_instance.read.assert_awaited_once()
crud_instance.get.assert_awaited_once()


@pytest.mark.anyio
async def test_crud_experience_create_or_update_new(
http_client: AsyncClient, db_session: Session
Expand All @@ -72,8 +118,8 @@ async def test_crud_experience_create_or_update_new(
# Get random experience data
data = ExperienceFactory.build_dict()

# Simulate a 'Not Found' experience by mocking the 'get' method
crud_instance.get = AsyncMock(return_value=None)
# Simulate not found experience by mocking the 'read' method
crud_instance.read = AsyncMock(return_value=[])

crud_instance.create = AsyncMock()
crud_instance.update = AsyncMock()
Expand All @@ -95,7 +141,8 @@ async def test_crud_experience_create_or_update_existing(
# Get random experience data
data = ExperienceFactory.build_dict(exclude={})

# Simulate an existing experience by mocking the 'get' method
# Simulate an existing experience by mocking the 'read' and 'get' methods
crud_instance.read = AsyncMock(return_value=[ExperienceRead(**data)])
crud_instance.get = AsyncMock(return_value=ExperienceRead(**data))

crud_instance.create = AsyncMock()
Expand Down
5 changes: 4 additions & 1 deletion src/api/core/warren/tests/xi/indexers/moodle/test_etl.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from pytest_httpx import HTTPXMock
from sqlmodel import Session

from warren.conf import settings
from warren.xi.client import ExperienceIndex
from warren.xi.enums import AggregationLevel, Structure
from warren.xi.factories import ExperienceFactory
Expand Down Expand Up @@ -56,6 +55,10 @@ async def test_course_content_factory(httpx_mock: HTTPXMock, db_session: Session
experience["updated_at"] = str(experience["updated_at"])

# Return the generated experience by mocking the XI response
httpx_mock.add_response(
url=re.compile(r".*experiences/\?iri.*"),
json=[experience],
)
httpx_mock.add_response(
url=re.compile(r".*experiences.*"),
json=experience,
Expand Down
74 changes: 35 additions & 39 deletions src/api/core/warren/tests/xi/routers/test_experiences.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,45 +239,6 @@ async def test_experience_read(
assert len(experiences) == number_experiences + 1


@pytest.mark.anyio
@pytest.mark.parametrize(
"number_experiences",
[100, 0],
)
async def test_experience_read_from_iri(
http_client: AsyncClient, db_session: Session, number_experiences: int
):
"""Test retrieving detailed information about an experience from its IRI."""
ExperienceFactory.__session__ = db_session

# Create some experiences in the database
if number_experiences:
ExperienceFactory.create_batch_sync(number_experiences)

# Create an experience
creation_date = ExperienceFactory.__faker__.date_time(timezone.utc)
with freeze_time(creation_date):
experience = ExperienceFactory.create_sync()

# Attempt to read the last created experience from its IRI
response = await http_client.get(f"/api/v1/experiences/{experience.iri}")
assert response.status_code == 200

# Verify the retrieved data matches the expected format
assert response.json() == {
**experience.model_dump(),
"id": str(experience.id),
"created_at": creation_date.isoformat(),
"updated_at": creation_date.isoformat(),
"relations_source": [],
"relations_target": [],
}

# Assert the database still contains the right number of experiences
experiences = db_session.exec(select(Experience)).all()
assert len(experiences) == number_experiences + 1


@pytest.mark.anyio
@pytest.mark.parametrize("invalid_id", ["foo", 123, "a1-a2-aa", uuid4().hex + "a"])
async def test_experience_read_invalid(
Expand Down Expand Up @@ -680,6 +641,7 @@ async def test_experiences_read_pagination(
{"structure": "Atomic"},
{"limit": 101},
{"offset": -1},
{"iri": -1},
],
)
async def test_experiences_read_invalid_params(
Expand Down Expand Up @@ -753,3 +715,37 @@ async def test_experiences_read_filter(
# Assert the database still contains the same number of experiences
experiences = db_session.exec(select(Experience)).all()
assert len(experiences) == 39


@pytest.mark.anyio
async def test_experiences_read_filter_on_iri(
http_client: AsyncClient, db_session: Session
):
"""Test reading an experience from its IRI."""
ExperienceFactory.__session__ = db_session

number_experiences = 10
ExperienceFactory.create_batch_sync(number_experiences)

# Create an experience
creation_date = ExperienceFactory.__faker__.date_time(timezone.utc)
with freeze_time(creation_date):
experience = ExperienceFactory.create_sync()

# Attempt to read the last created experience from its IRI
response = await http_client.get(
"/api/v1/experiences/", params={"iri": experience.iri}
)
assert response.status_code == 200

# Verify the retrieved data matches the expected format
assert response.json() == [
{
"id": str(experience.id),
"title": experience.title,
}
]

# Assert the database still contains the right number of experiences
experiences = db_session.exec(select(Experience)).all()
assert len(experiences) == number_experiences + 1
25 changes: 19 additions & 6 deletions src/api/core/warren/xi/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,27 @@ async def create(self, data: ExperienceCreate) -> UUID:
response.raise_for_status()
return UUID(response.json())

async def read(self, **kwargs) -> List[ExperienceReadSnapshot]:
"""Read multiple experiences while applying query params."""
response = await self._client.get(url=self._construct_url(""), params=kwargs)
response.raise_for_status()
return parse_obj_as(List[ExperienceReadSnapshot], response.json())

async def get_from_iri(self, object_id: IRI) -> Union[ExperienceRead, None]:
"""Get an experience from one of its IRI."""
db_experiences = await self.read(iri=object_id)

if not db_experiences:
return None

(db_experience,) = db_experiences
return await self.get(db_experience.id)

async def get(self, object_id: Union[UUID, IRI]) -> Union[ExperienceRead, None]:
"""Get an experience from one of its identifiers."""
if isinstance(object_id, IRI):
return await self.get_from_iri(object_id)

response = await self._client.get(url=self._construct_url(object_id))

if response.status_code == HTTPStatus.NOT_FOUND:
Expand All @@ -128,12 +147,6 @@ async def update(
response.raise_for_status()
return Experience(**response.json())

async def read(self, **kwargs) -> List[ExperienceReadSnapshot]:
"""Read multiple experiences while applying query params."""
response = await self._client.get(url=self._construct_url(""), params=kwargs)
response.raise_for_status()
return parse_obj_as(List[ExperienceReadSnapshot], response.json())

async def delete(self, **kwargs) -> None:
"""Delete an experience."""
raise NotImplementedError
Expand Down
5 changes: 3 additions & 2 deletions src/api/core/warren/xi/indexers/moodle/etl.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,10 @@ async def factory(
cls, source: LMS, target: ExperienceIndex, course_iri: IRI, **kwargs
):
"""Instantiate the class after fetching the given course."""
course = await target.experience.get(course_iri)
iri = IRI(course_iri)
course = await target.experience.get(iri)
if not course:
raise ValueError(f"Wrong course iri {course_iri}")
raise ValueError(f"Wrong course IRI {course_iri}")

return cls(lms=source, xi=target, course=course, **kwargs)

Expand Down
31 changes: 5 additions & 26 deletions src/api/core/warren/xi/routers/experiences.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ async def read_experiences(
session: Session = Depends(get_session),
structure: Optional[Structure] = None,
aggregation_level: Optional[AggregationLevel] = None,
iri: Optional[IRI] = None,
):
"""Retrieve a list of experiences based on query parameters.
Expand All @@ -43,6 +44,7 @@ async def read_experiences(
session (Session, optional): The database session.
structure (Structure, optional): Filter by experience structure.
aggregation_level (AggregationLevel, optional): Filter by aggregation level.
iri (IRI, optional): Filter by iri.
Returns:
List[ExperienceReadSnapshot]: List of experiences matching the query.
Expand All @@ -55,6 +57,9 @@ async def read_experiences(
statement = statement.where(Experience.structure == structure)
if aggregation_level:
statement = statement.where(Experience.aggregation_level == aggregation_level)
if iri:
statement = statement.where(Experience.iri == iri)

experiences = session.exec(
statement.offset(pagination.offset).limit(pagination.limit)
).all()
Expand Down Expand Up @@ -164,29 +169,3 @@ async def read_experience(experience_id: UUID, session: Session = Depends(get_se

logger.debug("Result = %s", experience)
return experience


@router.get("/{experience_iri:path}", response_model=ExperienceRead)
async def read_experience_from_iri(
experience_iri: IRI, session: Session = Depends(get_session)
):
"""Retrieve detailed information about an experience.
Args:
experience_iri (IRI): The IRI of the experience to retrieve.
session (Session, optional): The database session.
Returns:
ExperienceRead: Detailed information about the requested experience.
"""
logger.debug("Reading the experience")
experience = session.exec(
select(Experience).where(Experience.iri == experience_iri)
).one_or_none()
if not experience:
message = "Experience not found"
logger.debug("%s: %s", message, experience_iri)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=message)

logger.debug("Result = %s", experience)
return experience

0 comments on commit 826c473

Please sign in to comment.