Skip to content

Commit

Permalink
BAC-9152: add customer-results entity to SDK
Browse files Browse the repository at this point in the history
Merge in BAC/icometrix-sdk from feature/customer-results to master

Squashed commit of the following:

commit 156e8df02234061e10175f27fa3e9b8e0950427f
Author: Jeroen Pinxten <jeroen.pinxten@icometrix.com>
Date:   Thu Feb 15 10:40:47 2024 +0100

    BAC-9152: add customer-results entity to SDK
  • Loading branch information
jpinxten authored and Jesse Geens committed Feb 15, 2024
1 parent cded06c commit d7c6ca8
Show file tree
Hide file tree
Showing 15 changed files with 166 additions and 42 deletions.
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html

root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, root_path)

from icometrix_sdk._version import __version__
Expand Down Expand Up @@ -44,4 +44,4 @@
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output

html_theme = "furo"
html_static_path = ["_static"]
# html_static_path = ["_static"]
3 changes: 2 additions & 1 deletion docs/developer_guide/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ Developer Guide
.. toctree::

paginators
session
session
models
23 changes: 23 additions & 0 deletions docs/developer_guide/models.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Models
======

Some API operations may return incomplete results that require multiple requests to retrieve the entire
dataset. This process of fetching subsequent pages is known as pagination. Pagination is a crucial aspect when dealing
with large datasets to ensure efficient data retrieval.

Base model
----------

All entities that are returned by the icometrix API extend the :class:`~icometrix_sdk.models.base.BackendEntity` entity.
The BaseEntity always has the following properties:

- :attr:`~icometrix_sdk.models.base.BackendEntity.id`: The unique identifier (uuid)
- :attr:`~icometrix_sdk.models.base.BackendEntity.update_timestamp`: The timestamp of the last update
- :attr:`~icometrix_sdk.models.base.BackendEntity.creation_timestamp`: The creation timestamp
- :attr:`~icometrix_sdk.models.base.BackendEntity.uri`: The uri to point to the location of the object

Collections
-----------

When fetching a collection of models from the API; you will always get a subset of that collection in combination with
some extra meta data. See: :doc:`paginators`
6 changes: 6 additions & 0 deletions docs/models/customer_result.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Customer Report
===============

.. automodule:: icometrix_sdk.models.customer_result_entity
:members:
:undoc-members:
9 changes: 9 additions & 0 deletions docs/resources/customer_results.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Customer Results
================

.. automodule:: icometrix_sdk.resources.customer_results


.. autoclass:: icometrix_sdk.resources.customer_results.CustomerResults
:members:
:show-inheritance:
1 change: 1 addition & 0 deletions docs/resources/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ Resources
projects
patients
uploads
customer_results
customer_reports
35 changes: 11 additions & 24 deletions examples/upload_and_download.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import logging
import os
from pathlib import Path
from time import sleep
from typing import List, Dict
from typing import List

import pydicom
from pydicom.errors import InvalidDicomError

from icometrix_sdk import IcometrixApi
from icometrix_sdk.models.customer_report_entity import CustomerReportEntity
from icometrix_sdk.models.upload_entity import StartUploadDto

PROJECT_ID = "<uuid>"
Expand All @@ -20,9 +18,15 @@
logging.basicConfig(level=logging.INFO)


def extract_unique_studies(dir_path: str) -> List[str]:
def extract_unique_studies(folder_path: str) -> List[str]:
"""
Extract all study_instance_uids from DICOMS in a (sub)folder(s)
:param folder_path: The root path to start searching
:return:
"""
study_uids: List[str] = []
for path, subdirs, files in os.walk(dir_path):
for path, _, files in os.walk(folder_path):
for name in files:

file_path = os.path.join(path, name)
Expand All @@ -37,24 +41,6 @@ def extract_unique_studies(dir_path: str) -> List[str]:
return study_uids


def wait_for_customer_reports_to_finish(customer_reports: List[CustomerReportEntity]) -> List[CustomerReportEntity]:
finished_customer_reports: Dict[str, CustomerReportEntity] = {}
while len(finished_customer_reports) != len(customer_reports):
sleep(5)
for customer_report in customer_reports:
if customer_report.id in finished_customer_reports:
continue

report = ico_api.customer_reports.get_one(customer_report.uri)
if report.status != "Finished":
print(f"Waiting for {report.study_instance_uid} to complete: {report.status}")
continue

print(f"Finished {report.study_instance_uid}")
finished_customer_reports[report.id] = report
return [value for value in finished_customer_reports.values()]


if __name__ == '__main__':
os.environ["API_HOST"] = "https://icobrain-test.icometrix.com"

Expand All @@ -81,7 +67,8 @@ def wait_for_customer_reports_to_finish(customer_reports: List[CustomerReportEnt
customer_reports.append(csr)

# Wait for the reports to finish
customer_reports = wait_for_customer_reports_to_finish(customer_reports)
customer_report_uris = [customer_report.uri for customer_report in customer_reports]
customer_reports = ico_api.customer_reports.wait_for_results(customer_report_uris)

# Download icobrain report files
for customer_report in customer_reports:
Expand Down
6 changes: 6 additions & 0 deletions icometrix_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from icometrix_sdk.exceptions import IcometrixConfigException
from icometrix_sdk.resources.customer_reports import CustomerReports
from icometrix_sdk.models.base import PaginatedResponse
from icometrix_sdk.resources.customer_results import CustomerResults
from icometrix_sdk.resources.patients import Patients
from icometrix_sdk.resources.profile import Profile
from icometrix_sdk.resources.projects import Projects
Expand Down Expand Up @@ -39,6 +40,7 @@ class IcometrixApi:
patients: Patients
uploads: Uploads
customer_reports: CustomerReports
customer_results: CustomerResults

_api_client: ApiClient

Expand All @@ -52,9 +54,13 @@ def __init__(self, api_client: Optional[ApiClient] = None):
self.patients = Patients(self._api_client)
self.uploads = Uploads(self._api_client)
self.customer_reports = CustomerReports(self._api_client)
self.customer_results = CustomerResults(self._api_client)

def __enter__(self):
return self

def __exit__(self, *args, **kwargs):
pass
# todo force logout
# if self._api_client.auth:
# self._api_client.auth.disconnect(self._api_client)
6 changes: 6 additions & 0 deletions icometrix_sdk/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ def get_auth_method() -> Optional[AuthenticationMethod]:
Find an authentication method based on the 'AUTH_METHOD' environment variable
"""
auth_method = os.getenv("AUTH_METHOD")
if auth_method is None:
if os.getenv("TOKEN"):
auth_method = "token"
elif os.getenv("EMAIL") and os.getenv("PASSWORD"):
auth_method = "basic"

if auth_method == "basic":
return PasswordAuthentication(os.environ["EMAIL"], os.environ["PASSWORD"])
elif auth_method == "token":
Expand Down
2 changes: 0 additions & 2 deletions icometrix_sdk/models/customer_report_entity.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from typing import Optional, Union, List

from pydantic import BaseModel

from icometrix_sdk.models.base import BackendEntity


Expand Down
16 changes: 16 additions & 0 deletions icometrix_sdk/models/customer_result_entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from pydantic import BaseModel
from icometrix_sdk.models.base import BackendEntity


class CustomerResultPipelineResults(BaseModel):
pipeline_results_id: str


class CustomerResultEntity(BackendEntity):
uri: str
job_id: str
study_id: str
patient_id: str
project_id: str
qc_result_id: str
pipeline_results: CustomerResultPipelineResults
26 changes: 25 additions & 1 deletion icometrix_sdk/resources/customer_reports.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging
from time import sleep
from typing import Optional
from typing import Optional, Dict, List

from icometrix_sdk.exceptions import IcometrixDataImportException
from icometrix_sdk.models.base import PaginatedResponse
Expand Down Expand Up @@ -87,3 +87,27 @@ def wait_for_customer_report_for_study(self, project_id: str,
if count >= max_count:
raise IcometrixDataImportException(f"Failed to find a CustomerReport for {study_instance_uid}")
sleep(self.polling_interval)

def wait_for_results(self, customer_report_uris: List[str]) -> List[CustomerReportEntity]:
"""
Wait until processing has finished and the result files are available on the customer report
:param customer_report_uris: A list of customer uris
:return:
"""

finished_customer_reports: Dict[str, CustomerReportEntity] = {}
while len(finished_customer_reports) != len(customer_report_uris):
for customer_report_uri in customer_report_uris:
if customer_report_uri in finished_customer_reports:
continue

report = self.get_one(customer_report_uri)
if report.status != "Finished":
print(f"Waiting for {report.study_instance_uid} to complete: {report.status}")
continue

print(f"Finished {report.study_instance_uid}")
finished_customer_reports[report.uri] = report
sleep(self.polling_interval)
return [value for value in finished_customer_reports.values()]
45 changes: 45 additions & 0 deletions icometrix_sdk/resources/customer_results.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import logging

from icometrix_sdk.models.base import PaginatedResponse
from icometrix_sdk.models.customer_result_entity import CustomerResultEntity
from icometrix_sdk.utils.requests_api_client import ApiClient

logger = logging.getLogger(__name__)


class CustomerResults:
def __init__(self, api: ApiClient):
self._api = api

def get_all_for_study(self, study_uri: str, **kwargs) -> PaginatedResponse[CustomerResultEntity]:
"""
Get al customer reports for a project
:param study_uri: The uri of a study
:return: A Paginated response containing customer-results
"""

page = self._api.get(f"{study_uri}/customer-results", **kwargs)
return PaginatedResponse[CustomerResultEntity](**page)

def get_all_for_pipeline_result(self, pipeline_result_uri: str, **kwargs) -> \
PaginatedResponse[CustomerResultEntity]:
"""
Get al customer reports for a pipeline-result
:param pipeline_result_uri: The uri of a pipeline-result
:return: A Paginated response containing customer-results
"""

page = self._api.get(f"{pipeline_result_uri}/customer-results", **kwargs)
return PaginatedResponse[CustomerResultEntity](**page)

def get_one(self, customer_result_uri: str) -> CustomerResultEntity:
"""
Get a single customer-result based on the customer-result uri
:param customer_result_uri: the uri of the customer-result
:return: A single customer-result or 404
"""
resp = self._api.get(customer_result_uri)
return CustomerResultEntity(**resp)
18 changes: 10 additions & 8 deletions icometrix_sdk/utils/paginator.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from inspect import signature
from typing import Callable, Generic, TypeVar, Optional
from typing import Callable, Generic, TypeVar, Optional, Iterable, Any, ParamSpec

from icometrix_sdk.models.base import PaginatedResponse

T = TypeVar("T")
P = ParamSpec("P")


def can_paginate(func: Callable):
Expand All @@ -14,7 +15,7 @@ def can_paginate(func: Callable):
return issubclass(sig.return_annotation, PaginatedResponse)


class PageIterator(Generic[T]):
class PageIterator(Generic[T], Iterable):
"""
An iterable object to iterate over paginated api responses
"""
Expand Down Expand Up @@ -55,7 +56,9 @@ def _fetch_current_page(self) -> PaginatedResponse[T]:
return self._func(**self._op_kwargs)


def get_paginator(func: Callable, page_size: Optional[int] = 50, starting_index: Optional[int] = 0, **kwargs):
def get_paginator(func: Callable[..., PaginatedResponse[T]],
page_size: Optional[int] = 50,
starting_index: Optional[int] = 0, **kwargs) -> PageIterator[T]:
"""
Create paginator object for an operation.
Expand All @@ -70,12 +73,11 @@ def get_paginator(func: Callable, page_size: Optional[int] = 50, starting_index:
The size of the pages
:param starting_index:
The starting page
:returns: Iterator
:returns: A PageIterator
"""
if not can_paginate(func):
raise ValueError(f"Function '{func.__name__}' can't be paginated")

sig = signature(func)
return iter(
PageIterator[sig.return_annotation](func, op_kwargs=kwargs, page_size=page_size, starting_index=starting_index)
)
# sig = signature(func)
return PageIterator[T](func, op_kwargs=kwargs, page_size=page_size,
starting_index=starting_index)
8 changes: 4 additions & 4 deletions icometrix_sdk/utils/requests_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class RequestsApiClient(ApiClient):
_auth_attempts = 0

_session: Session
_auth: Optional[AuthenticationMethod]
auth: Optional[AuthenticationMethod]

def __init__(self, server: str, auth: Optional[AuthenticationMethod] = None):
self.base_headers = {
Expand All @@ -45,7 +45,7 @@ def __init__(self, server: str, auth: Optional[AuthenticationMethod] = None):
"Content-Type": "application/json"
}
self.server = server
self._auth = auth
self.auth = auth

if not server:
raise IcometrixConfigException("Server is required")
Expand All @@ -54,10 +54,10 @@ def __init__(self, server: str, auth: Optional[AuthenticationMethod] = None):
self._authenticate()

def _authenticate(self):
if self._auth:
if self.auth:
logger.info("Authenticating")
self._auth_attempts += 1
self._auth.connect(self)
self.auth.connect(self)
self._auth_attempts = 0

def _refresh_token(self, resp: Response):
Expand Down

0 comments on commit d7c6ca8

Please sign in to comment.