From 01fc3d829ff38f2be87fe4d93c5227e710a51c40 Mon Sep 17 00:00:00 2001 From: m-peko Date: Thu, 21 Aug 2025 12:09:54 +0200 Subject: [PATCH 1/4] Rename timeout param to timeout_seconds --- examples/async_client.py | 2 +- examples/client.py | 2 +- examples/client_simple.py | 2 +- src/atlas/models/evaluation.py | 8 ++++---- src/atlas/resources/evaluations/evaluations.py | 16 ++++++++++------ 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/examples/async_client.py b/examples/async_client.py index 5686f6f..a2e1a56 100644 --- a/examples/async_client.py +++ b/examples/async_client.py @@ -28,7 +28,7 @@ async def main(): evaluation = await client.evaluations.wait_for_completion( evaluation, interval_seconds=10, - timeout=600, # 10 minutes + timeout_seconds=600, # 10 minutes ) print(f"Evaluation {evaluation.id} finished with status={evaluation.status}") diff --git a/examples/client.py b/examples/client.py index 4bdc076..9661215 100644 --- a/examples/client.py +++ b/examples/client.py @@ -24,7 +24,7 @@ evaluation = client.evaluations.wait_for_completion( evaluation, interval_seconds=10, - timeout=600, # 10 minutes + timeout_seconds=600, # 10 minutes ) print(f"Evaluation {evaluation.id} finished with status={evaluation.status}") diff --git a/examples/client_simple.py b/examples/client_simple.py index 007c850..12134ad 100644 --- a/examples/client_simple.py +++ b/examples/client_simple.py @@ -24,7 +24,7 @@ # --- Wait for completion evaluation.wait_for_completion( interval_seconds=10, - timeout=600, # 10 minutes + timeout_seconds=600, # 10 minutes ) print(f"Evaluation {evaluation.id} finished with status={evaluation.status}") diff --git a/src/atlas/models/evaluation.py b/src/atlas/models/evaluation.py index 0603b48..2879563 100644 --- a/src/atlas/models/evaluation.py +++ b/src/atlas/models/evaluation.py @@ -88,7 +88,7 @@ async def get_results_async( return await self._client.results.get(evaluation=self, page=page, page_size=page_size, timeout=timeout) def wait_for_completion( - self, *, interval_seconds: int = 30, timeout: Optional[float] = None + self, *, interval_seconds: int = 30, timeout_seconds: Optional[int] = None ) -> Optional["Evaluation"]: """Sync polling using a sync client.""" from .._client import AsyncAtlas @@ -99,7 +99,7 @@ def wait_for_completion( raise RuntimeError("Use `wait_for_completion_async()` with an async client") evaluation = self._client.evaluations.wait_for_completion( - self, interval_seconds=interval_seconds, timeout=timeout + self, interval_seconds=interval_seconds, timeout_seconds=timeout_seconds ) if evaluation: self.status = evaluation.status @@ -110,7 +110,7 @@ def wait_for_completion( return self async def wait_for_completion_async( - self, *, interval_seconds: int = 30, timeout: Optional[float] = None + self, *, interval_seconds: int = 30, timeout_seconds: Optional[int] = None ) -> Optional["Evaluation"]: """Async polling using an async client.""" from .._client import AsyncAtlas @@ -121,7 +121,7 @@ async def wait_for_completion_async( raise RuntimeError("Use `wait_for_completion()` with a sync client") evaluation = await self._client.evaluations.wait_for_completion( - self, interval_seconds=interval_seconds, timeout=timeout + self, interval_seconds=interval_seconds, timeout_seconds=timeout_seconds ) if evaluation: self.status = evaluation.status diff --git a/src/atlas/resources/evaluations/evaluations.py b/src/atlas/resources/evaluations/evaluations.py index 192272c..63ea013 100644 --- a/src/atlas/resources/evaluations/evaluations.py +++ b/src/atlas/resources/evaluations/evaluations.py @@ -74,15 +74,17 @@ def wait_for_completion( evaluation: Evaluation, *, interval_seconds: int = 30, - timeout: float | None = None, + timeout_seconds: int | None = None, ) -> Optional[Evaluation]: """Poll until the evaluation finishes or timeout is reached.""" start = time.time() updated_evaluation: Optional[Evaluation] = self.get(evaluation) while updated_evaluation and not updated_evaluation.is_finished: - if timeout and (time.time() - start) > timeout: - raise TimeoutError(f"Evaluation {updated_evaluation.id} did not complete within {timeout} seconds") + if timeout_seconds and (time.time() - start) > timeout_seconds: + raise TimeoutError( + f"Evaluation {updated_evaluation.id} did not complete within {timeout_seconds} seconds" + ) time.sleep(interval_seconds) updated_evaluation = self.get(updated_evaluation) @@ -146,15 +148,17 @@ async def wait_for_completion( evaluation: Evaluation, *, interval_seconds: int = 30, - timeout: Optional[float] = None, + timeout_seconds: Optional[int] = None, ) -> Optional[Evaluation]: """Poll asynchronously until the evaluation finishes or timeout is reached.""" start = asyncio.get_event_loop().time() updated_evaluation: Optional[Evaluation] = await self.get(evaluation) while updated_evaluation and not updated_evaluation.is_finished: - if timeout and (asyncio.get_event_loop().time() - start) > timeout: - raise TimeoutError(f"Evaluation {updated_evaluation.id} did not complete within {timeout} seconds") + if timeout_seconds and (asyncio.get_event_loop().time() - start) > timeout_seconds: + raise TimeoutError( + f"Evaluation {updated_evaluation.id} did not complete within {timeout_seconds} seconds" + ) await asyncio.sleep(interval_seconds) updated_evaluation = await self.get(updated_evaluation) From 7a22089f051a36b606d62e6af7cb2f95b443de7a Mon Sep 17 00:00:00 2001 From: m-peko Date: Thu, 21 Aug 2025 12:41:30 +0200 Subject: [PATCH 2/4] Avoid using factory create function on AsyncAtlas --- examples/async_client.py | 2 +- examples/async_client_simple.py | 2 +- pyproject.toml | 7 +++-- requirements-dev.lock | 10 +++++++ requirements.lock | 8 ++++++ src/atlas/_client.py | 51 +++++++++++---------------------- 6 files changed, 41 insertions(+), 39 deletions(-) diff --git a/examples/async_client.py b/examples/async_client.py index a2e1a56..ccc2efb 100644 --- a/examples/async_client.py +++ b/examples/async_client.py @@ -7,7 +7,7 @@ async def main(): # Construct async client - client = await AsyncAtlas.create() + client = AsyncAtlas() # --- Models models = await client.models.get() diff --git a/examples/async_client_simple.py b/examples/async_client_simple.py index b1bd85c..30db56a 100644 --- a/examples/async_client_simple.py +++ b/examples/async_client_simple.py @@ -7,7 +7,7 @@ async def main(): # Construct async client - client = await AsyncAtlas.create() + client = AsyncAtlas() # --- Models models = await client.models.get() diff --git a/pyproject.toml b/pyproject.toml index 2d47a68..db873b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "1.0.0" description = "The official Python library for the LayerLens Atlas API" license = "Apache-2.0" authors = [{ name = "LayerLens", email = "support@layerlens.ai" }] -dependencies = ["httpx>=0.23.0, <1", "pydantic>=1.9.0, <3"] +dependencies = ["httpx>=0.23.0, <1", "pydantic>=1.9.0, <3", "requests"] requires-python = ">= 3.8" classifiers = [ "Typing :: Typed", @@ -35,11 +35,12 @@ atlas = "atlas.cli:main" managed = true # version pins are in requirements-dev.lock dev-dependencies = [ - "pyright==1.1.399", "mypy", "pytest", - "ruff", + "pyright==1.1.399", "pytest-cov>=6.2.1", + "ruff", + "types-requests", ] [tool.rye.scripts] diff --git a/requirements-dev.lock b/requirements-dev.lock index 7d97580..9c0f730 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -17,6 +17,9 @@ anyio==4.9.0 certifi==2025.7.14 # via httpcore # via httpx + # via requests +charset-normalizer==3.4.3 + # via requests coverage==7.10.2 # via pytest-cov exceptiongroup==1.3.0 @@ -31,6 +34,7 @@ httpx==0.28.1 idna==3.10 # via anyio # via httpx + # via requests iniconfig==2.1.0 # via pytest mypy==1.17.0 @@ -55,6 +59,8 @@ pyright==1.1.399 pytest==8.4.1 # via pytest-cov pytest-cov==6.2.1 +requests==2.32.5 + # via atlas ruff==0.12.7 sniffio==1.3.1 # via anyio @@ -62,6 +68,7 @@ tomli==2.2.1 # via coverage # via mypy # via pytest +types-requests==2.32.4.20250809 typing-extensions==4.14.1 # via anyio # via exceptiongroup @@ -72,3 +79,6 @@ typing-extensions==4.14.1 # via typing-inspection typing-inspection==0.4.1 # via pydantic +urllib3==2.5.0 + # via requests + # via types-requests diff --git a/requirements.lock b/requirements.lock index a70f53e..887d3cc 100644 --- a/requirements.lock +++ b/requirements.lock @@ -17,6 +17,9 @@ anyio==4.9.0 certifi==2025.7.14 # via httpcore # via httpx + # via requests +charset-normalizer==3.4.3 + # via requests exceptiongroup==1.3.0 # via anyio h11==0.16.0 @@ -28,10 +31,13 @@ httpx==0.28.1 idna==3.10 # via anyio # via httpx + # via requests pydantic==2.11.7 # via atlas pydantic-core==2.33.2 # via pydantic +requests==2.32.5 + # via atlas sniffio==1.3.1 # via anyio typing-extensions==4.14.1 @@ -42,3 +48,5 @@ typing-extensions==4.14.1 # via typing-inspection typing-inspection==0.4.1 # via pydantic +urllib3==2.5.0 + # via requests diff --git a/src/atlas/_client.py b/src/atlas/_client.py index 704da92..587fc7a 100644 --- a/src/atlas/_client.py +++ b/src/atlas/_client.py @@ -7,6 +7,7 @@ from typing_extensions import Self, override import httpx +import requests from . import _exceptions from ._utils import is_mapping @@ -98,10 +99,7 @@ def results(self) -> Results: @property @override def auth_headers(self) -> dict[str, str]: - api_key = self.api_key - if not api_key: - return {} - return {"x-api-key": api_key} + return {"x-api-key": self.api_key} if self.api_key else {} def copy( self, @@ -166,9 +164,7 @@ def _get_organization(self) -> Optional[Organization]: timeout=30, cast_to=OrganizationResponse, ) - if isinstance(organization, OrganizationResponse): - return organization.data - return None + return organization.data if isinstance(organization, OrganizationResponse) else None class AsyncAtlas(BaseAsyncClient): @@ -204,31 +200,16 @@ def __init__( super().__init__(base_url=base_url, timeout=timeout) - # org/project must be fetched asynchronously - self.organization_id = None - self.project_id = None - - @classmethod - async def create( - cls, - *, - api_key: str | None = None, - base_url: str | httpx.URL | None = None, - timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT, - ) -> "AsyncAtlas": - """Async factory that combines __init__ and ainit into one call.""" - self = cls(api_key=api_key, base_url=base_url, timeout=timeout) - organization = await self._get_organization() + organization = self._get_organization() if organization is None: - raise AtlasError("Organization could not be fetched. Please contact LayerLens Atlas support.") + raise AtlasError(f"Organization could not be fetched. Please contact LayerLens Atlas support.") self.organization_id = organization.id - if not organization.projects: + if organization.projects is None or len(organization.projects) == 0: raise AtlasError( f"Organization {self.organization_id} is missing project. Please contact LayerLens Atlas support." ) self.project_id = organization.projects[0].id - return self @cached_property def benchmarks(self) -> AsyncBenchmarks: @@ -255,6 +236,7 @@ def results(self) -> AsyncResults: return AsyncResults(self) @property + @override def auth_headers(self) -> dict[str, str]: return {"x-api-key": self.api_key} if self.api_key else {} @@ -304,15 +286,16 @@ def _make_status_error( return APIStatusError(err_msg, response=response, body=data) - async def _get_organization(self) -> Optional[Organization]: - organization = await super().get_cast( - "/organizations", - timeout=30, - cast_to=OrganizationResponse, - ) - if isinstance(organization, OrganizationResponse): - return organization.data - return None + def _get_organization(self) -> Optional[Organization]: + url = f"{self.base_url}organizations" + + response = requests.get(url, headers=self.default_headers, timeout=30) + response.raise_for_status() + + data = response.json() + + organization = OrganizationResponse(**data) + return organization.data if isinstance(organization, OrganizationResponse) else None Client = Atlas From 772d88faed31c39e009aeb4b8b15f2faaf909f16 Mon Sep 17 00:00:00 2001 From: m-peko Date: Thu, 21 Aug 2025 13:25:31 +0200 Subject: [PATCH 3/4] Add function for fetching all the results --- src/atlas/models/evaluation.py | 32 ++++- src/atlas/resources/results/results.py | 160 ++++++++++++++++++++++++- 2 files changed, 186 insertions(+), 6 deletions(-) diff --git a/src/atlas/models/evaluation.py b/src/atlas/models/evaluation.py index 2879563..fc5b2bd 100644 --- a/src/atlas/models/evaluation.py +++ b/src/atlas/models/evaluation.py @@ -1,7 +1,7 @@ from __future__ import annotations from enum import Enum -from typing import TYPE_CHECKING, Dict, Optional +from typing import TYPE_CHECKING, Dict, List, Optional from datetime import timedelta import httpx @@ -70,6 +70,21 @@ def get_results( return self._client.results.get(evaluation=self, page=page, page_size=page_size, timeout=timeout) + def get_all_results( + self, + *, + timeout: float | httpx.Timeout | None = None, + ) -> List[Result]: + """Fetch results synchronously if a sync client is attached.""" + from .._client import AsyncAtlas + + if self._client is None: + raise ValueError("No client attached") + if isinstance(self._client, AsyncAtlas): + raise RuntimeError("Use `await get_results_async()` with an async client") + + return self._client.results.get_all(evaluation=self, timeout=timeout) + async def get_results_async( self, *, @@ -87,6 +102,21 @@ async def get_results_async( return await self._client.results.get(evaluation=self, page=page, page_size=page_size, timeout=timeout) + async def get_all_results_async( + self, + *, + timeout: float | httpx.Timeout | None = None, + ) -> List[Result]: + """Fetch results asynchronously if an async client is attached.""" + from .._client import AsyncAtlas + + if self._client is None: + raise ValueError("No client attached") + if not isinstance(self._client, AsyncAtlas): + raise RuntimeError("Use `get_results()` with a sync client") + + return await self._client.results.get_all(evaluation=self, timeout=timeout) + def wait_for_completion( self, *, interval_seconds: int = 30, timeout_seconds: Optional[int] = None ) -> Optional["Evaluation"]: diff --git a/src/atlas/resources/results/results.py b/src/atlas/resources/results/results.py index df8cc26..46bb097 100644 --- a/src/atlas/resources/results/results.py +++ b/src/atlas/resources/results/results.py @@ -1,11 +1,11 @@ from __future__ import annotations import math -from typing import Optional +from typing import List, Optional import httpx -from ...models import Evaluation, ResultsResponse +from ...models import Result, Evaluation, ResultsResponse from ..._resource import SyncAPIResource, AsyncAPIResource from ..._constants import DEFAULT_TIMEOUT @@ -22,6 +22,23 @@ def get( page_size: Optional[int] = None, timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT, ) -> ResultsResponse | None: + """ + Get evaluation results with optional pagination. + + Args: + evaluation: evaluation to get the results for + page: Page number for pagination (1-based, defaults to 1 if not provided) + page_size: Number of results per page (default: 100, optional) + timeout: Request timeout + + Returns: + ResultsResponse object containing: + - evaluation_id: The evaluation ID + - results: List of Result objects for the current page + - metrics: Contains total_count and score ranges + - pagination: Calculated pagination info + or None if the request fails + """ return self.get_by_id(evaluation_id=evaluation.id, page=page, page_size=page_size, timeout=timeout) def get_by_id( @@ -36,7 +53,7 @@ def get_by_id( Get evaluation results with optional pagination. Args: - evaluation: Evaluation to get the results for + evaluation_id: ID of evaluation to get the results for page: Page number for pagination (1-based, defaults to 1 if not provided) page_size: Number of results per page (default: 100, optional) timeout: Request timeout @@ -46,7 +63,7 @@ def get_by_id( - evaluation_id: The evaluation ID - results: List of Result objects for the current page - metrics: Contains total_count and score ranges - - pagination: Calculated pagination info (total_count, page_size, total_pages) + - pagination: Calculated pagination info or None if the request fails """ params = {"evaluation_id": evaluation_id} @@ -90,6 +107,64 @@ def get_by_id( except Exception: return None + def get_all( + self, + *, + evaluation: Evaluation, + timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT, + ) -> List[Result]: + """ + Fetch all results for the given evaluation by iterating over all pages. + + Args: + evaluation: Evaluation to get the results for + timeout: Request timeout + + Returns: + List of all Result objects across all pages. + """ + return self.get_all_by_id(evaluation_id=evaluation.id, timeout=timeout) + + def get_all_by_id( + self, + *, + evaluation_id: str, + timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT, + ) -> List[Result]: + """ + Fetch all results for the given evaluation by iterating over all pages. + + Args: + evaluation_id: ID of evaluation to get the results for + timeout: Request timeout + + Returns: + List of all Result objects across all pages. + """ + all_results: List[Result] = [] + current_page = 1 + + while True: + resp = self.get_by_id( + evaluation_id=evaluation_id, + page=current_page, + page_size=DEFAULT_PAGE_SIZE, + timeout=timeout, + ) + + if resp is None or not resp.results: + break + + all_results.extend(resp.results) + + # Stop if we reached the last page + if resp.pagination.page >= resp.pagination.total_pages: + break + + current_page += 1 + + return all_results + class AsyncResults(AsyncAPIResource): async def get( @@ -100,6 +175,23 @@ async def get( page_size: Optional[int] = None, timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT, ) -> ResultsResponse | None: + """ + Get evaluation results with optional pagination. + + Args: + evaluation: Evaluation to get the results for + page: Page number for pagination (1-based, defaults to 1 if not provided) + page_size: Number of results per page (default: 100, optional) + timeout: Request timeout + + Returns: + ResultsResponse object containing: + - evaluation_id: The evaluation ID + - results: List of Result objects for the current page + - metrics: Contains total_count and score ranges + - pagination: Calculated pagination info (total_count, page_size, total_pages) + or None if the request fails + """ return await self.get_by_id(evaluation_id=evaluation.id, page=page, page_size=page_size, timeout=timeout) async def get_by_id( @@ -114,7 +206,7 @@ async def get_by_id( Get evaluation results with optional pagination. Args: - evaluation: Evaluation to get the results for + evaluation_id: ID of evaluation to get the results for page: Page number for pagination (1-based, defaults to 1 if not provided) page_size: Number of results per page (default: 100, optional) timeout: Request timeout @@ -166,3 +258,61 @@ async def get_by_id( return ResultsResponse.model_validate(resp_with_pagination) except Exception: return None + + async def get_all( + self, + *, + evaluation: Evaluation, + timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT, + ) -> List[Result]: + """ + Fetch all results for the given evaluation by iterating over all pages. + + Args: + evaluation: Evaluation to get the results for + timeout: Request timeout + + Returns: + List of all Result objects across all pages. + """ + return await self.get_all_by_id(evaluation_id=evaluation.id, timeout=timeout) + + async def get_all_by_id( + self, + *, + evaluation_id: str, + timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT, + ) -> List[Result]: + """ + Fetch all results for the given evaluation by iterating over all pages. + + Args: + evaluation_id: ID of evaluation to get the results for + timeout: Request timeout + + Returns: + List of all Result objects across all pages. + """ + all_results: List[Result] = [] + current_page = 1 + + while True: + resp = await self.get_by_id( + evaluation_id=evaluation_id, + page=current_page, + page_size=DEFAULT_PAGE_SIZE, + timeout=timeout, + ) + + if resp is None or not resp.results: + break + + all_results.extend(resp.results) + + # Stop if we reached the last page + if resp.pagination.page >= resp.pagination.total_pages: + break + + current_page += 1 + + return all_results From 6a028772ffd71ba1c85bfbca6f8f4f10b732b4e0 Mon Sep 17 00:00:00 2001 From: m-peko Date: Thu, 21 Aug 2025 14:19:11 +0200 Subject: [PATCH 4/4] Minor cleanup --- src/atlas/resources/evaluations/evaluations.py | 2 +- src/atlas/resources/results/results.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/atlas/resources/evaluations/evaluations.py b/src/atlas/resources/evaluations/evaluations.py index 63ea013..c2664fe 100644 --- a/src/atlas/resources/evaluations/evaluations.py +++ b/src/atlas/resources/evaluations/evaluations.py @@ -74,7 +74,7 @@ def wait_for_completion( evaluation: Evaluation, *, interval_seconds: int = 30, - timeout_seconds: int | None = None, + timeout_seconds: Optional[int] = None, ) -> Optional[Evaluation]: """Poll until the evaluation finishes or timeout is reached.""" start = time.time() diff --git a/src/atlas/resources/results/results.py b/src/atlas/resources/results/results.py index 46bb097..09f8d0d 100644 --- a/src/atlas/resources/results/results.py +++ b/src/atlas/resources/results/results.py @@ -20,7 +20,7 @@ def get( evaluation: Evaluation, page: Optional[int] = None, page_size: Optional[int] = None, - timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT, + timeout: Optional[float | httpx.Timeout] = DEFAULT_TIMEOUT, ) -> ResultsResponse | None: """ Get evaluation results with optional pagination. @@ -47,7 +47,7 @@ def get_by_id( evaluation_id: str, page: Optional[int] = None, page_size: Optional[int] = None, - timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT, + timeout: Optional[float | httpx.Timeout] = DEFAULT_TIMEOUT, ) -> ResultsResponse | None: """ Get evaluation results with optional pagination. @@ -111,7 +111,7 @@ def get_all( self, *, evaluation: Evaluation, - timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT, + timeout: Optional[float | httpx.Timeout] = DEFAULT_TIMEOUT, ) -> List[Result]: """ Fetch all results for the given evaluation by iterating over all pages. @@ -129,7 +129,7 @@ def get_all_by_id( self, *, evaluation_id: str, - timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT, + timeout: Optional[float | httpx.Timeout] = DEFAULT_TIMEOUT, ) -> List[Result]: """ Fetch all results for the given evaluation by iterating over all pages. @@ -173,7 +173,7 @@ async def get( evaluation: Evaluation, page: Optional[int] = None, page_size: Optional[int] = None, - timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT, + timeout: Optional[float | httpx.Timeout] = DEFAULT_TIMEOUT, ) -> ResultsResponse | None: """ Get evaluation results with optional pagination. @@ -200,7 +200,7 @@ async def get_by_id( evaluation_id: str, page: Optional[int] = None, page_size: Optional[int] = None, - timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT, + timeout: Optional[float | httpx.Timeout] = DEFAULT_TIMEOUT, ) -> ResultsResponse | None: """ Get evaluation results with optional pagination. @@ -263,7 +263,7 @@ async def get_all( self, *, evaluation: Evaluation, - timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT, + timeout: Optional[float | httpx.Timeout] = DEFAULT_TIMEOUT, ) -> List[Result]: """ Fetch all results for the given evaluation by iterating over all pages. @@ -281,7 +281,7 @@ async def get_all_by_id( self, *, evaluation_id: str, - timeout: float | httpx.Timeout | None = DEFAULT_TIMEOUT, + timeout: Optional[float | httpx.Timeout] = DEFAULT_TIMEOUT, ) -> List[Result]: """ Fetch all results for the given evaluation by iterating over all pages.