Skip to content

Commit

Permalink
Renamed package and clients to match Sectra's nomenclature; moved cli…
Browse files Browse the repository at this point in the history
…ent in contrib/python_modules/
  • Loading branch information
SebLefort authored and sectra-masve committed Jan 9, 2025
1 parent a41af36 commit fb0004c
Show file tree
Hide file tree
Showing 25 changed files with 78 additions and 78 deletions.
File renamed without changes.
File renamed without changes.
14 changes: 14 additions & 0 deletions contrib/python_modules/sectra_dpat_client/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
SHELL=bash

.PHONY: format
format:
isort .;
black .

.PHONY: lint
lint:
flake8 sectra_dpat_client/
bandit -c .bandit.yml -r sectra_dpat_client/
isort --check sectra_dpat_client/
black sectra_dpat_client/ --check --diff
mypy sectra_dpat_client/
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Python Client for Sectra IDS7 PACS
# Python Client for Sectra DPAT PACS

## Introduction

This python package aims to facilite the development of AI applications integrated in Sectra IDS7 PACS.
This python package aims to facilite the development of AI applications integrated in Sectra DPAT PACS.

Please note that for now, not every feature is implemented but the package can easily be enriched:

Expand All @@ -11,22 +11,22 @@ Please note that for now, not every feature is implemented but the package can e

### What it implements

* A client for the IDS7 AI API (`IDS7AIClient`)
* A client for the IDS7 QIDO API (`IDS7QidoClient`)
* A set of pydantic models to encapsulate and validate data sent and received from IDS7
* A client for the DPAT AI API (`DPATAIClient`)
* A client for the DPAT QIDO API (`DPATQidoClient`)
* A set of pydantic models to encapsulate and validate data sent and received from DPAT


### What it does not implement, but might, at some point

* A server to receive analysis requests from IDS7
* A server to receive analysis requests from DPAT

## Installation

To install ids7client (until it is published in pypi):
To install sectra_dpat_client (until it is published in pypi):

```pip install .```

To install ids7client with development dependencies (linting and formating, see below):
To install sectra_dpat_client with development dependencies (linting and formating, see below):

```pip install ./[dev]```

Expand All @@ -37,16 +37,16 @@ Before using the client, make sure you have access to a valid authentication tok
### Example 1: Retrieve image information

```python
from ids7client.ai import IDS7AIClient
from sectra_dpat_client.ai import DPATAIClient

# Callback info, sent by IDS7 in the request
# Callback info, sent by DPAT in the request
callback_url = "https://sectrapacs.com"
callback_token = "abc"

# Slide id, sent by IDS7 in the request
# Slide id, sent by DPAT in the request
slide_id = "blabla"

client = IDS7AIClient(
client = DPATAIClient(
url=callback_url,
token=callback_token
)
Expand All @@ -60,7 +60,7 @@ image_info = client.get_image_info(slide_id, extended=True, phi=True)
The following code creates a result payload that can be sent as response to display a frame on the whole slide and a label telling the user that the analysis is pending.

```python
from ids7client.ai import (
from sectra_dpat_client.ai import (
ResultContent,
ResultType,
PrimitiveItem,
Expand All @@ -71,7 +71,7 @@ from ids7client.ai import (
Result
)

# Slide id, sent by IDS7 in the request
# Slide id, sent by DPAT in the request
slide_id = "blabla"

data = ResultContent(
Expand Down Expand Up @@ -112,17 +112,17 @@ result = Result(
### Example 3: Retrieving patient, request and exam id

```python
from IDS7Client.ai import IDS7AIClient
from IDS7Client.qido import IDS7QidoClient, DicomCodes
from sectra_dpat_client.ai import DPATAIClient
from sectra_dpat_client.qido import DPATQidoClient, DicomCodes

# Callback info, sent by IDS7 in the request
# Callback info, sent by DPAT in the request
callback_url = "https://sectrapacs.com"
callback_token = "abc"

# Slide id, sent by IDS7 in the request
# Slide id, sent by DPAT in the request
slide_id = "blabla"

ai_client = IDS7AIClient(
ai_client = DPATAIClient(
url=callback_url,
token=callback_token
)
Expand All @@ -131,7 +131,7 @@ ai_client = IDS7AIClient(
image_info = client.get_image_info(slide_id, extended=True, phi=True)

# Instanciates QIDO client with provided url, username and password
qido_client = IDS7QidoClient(qido_url, qido_username, qido_password)
qido_client = DPATQidoClient(qido_url, qido_username, qido_password)

# Retrieve study from QIDO API
study = qido_client.find_one_study(studyInstanceUid=data.study_id)
Expand All @@ -144,9 +144,9 @@ exam_id = study.get_value_as_string(DicomCodes.EXAM_ID)

### Error handling and retries

Any request to the IDS7 server is retried 5 times with exponential delays if there is a connection error. Any other error is not handled by the clients.
Any request to the DPAT server is retried 5 times with exponential delays if there is a connection error. Any other error is not handled by the clients.

Clients raise `IDS7RequestError` if the IDS7 server returns an error status code (e.g., 400, 404, 500, etc.). The error includes the returned status code, text and the requested path.
Clients raise `DPATRequestError` if the DPAT server returns an error status code (e.g., 400, 404, 500, etc.). The error includes the returned status code, text and the requested path.

## Code quality

Expand All @@ -171,4 +171,4 @@ One can lint the code using the following command:

* HL7 API client
* More detailed data validation (e.g., min and max lengths of arrays)
* Missing IDS7 endpoints
* Missing DPAT endpoints
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .errors import DPATRequestError
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .client import IDS7AIClient
from .client import DPATAIClient
from .schemas import (
Action,
ApplicationInfo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,37 @@

import requests

from ids7client.errors import IDS7RequestError
from ids7client.helpers import JSONPayload, connection_retry
from sectra_dpat_client.errors import DPATRequestError
from sectra_dpat_client.helpers import JSONPayload, connection_retry

from .schemas import ApplicationInfo, ImageInfo, Result, ResultResponse

logger = logging.getLogger(__name__)


class IDS7AIClient:
"""Class managing connection and requests to IDS7 server AI API.
class DPATAIClient:
"""Class managing connection and requests to DPAT server AI API.
Args:
url (str): URL of the IDS7 server
url (str): URL of the DPAT server
token (str): Callback token
app_id (str): Registered application id
Attributes:
ids7_version (ApplicationInfo): Versions of the IDS7 server
dpat_version (ApplicationInfo): Versions of the DPAT server
"""

__slots__ = ("_url", "_token", "ids7_version", "_headers", "_app_id")
__slots__ = ("_url", "_token", "dpat_version", "_headers", "_app_id")

def __init__(self, url: str, token: str, app_id: str) -> None:
self._url = url
self._token = token
self._app_id = app_id
self._headers = {"Authorization": f"Bearer {token}"}
self.ids7_version = self._retrieve_ids7_versions()
self.dpat_version = self._retrieve_dpat_versions()

def _retrieve_ids7_versions(self) -> ApplicationInfo:
"""Retrieves the versions of IDS7 from the server."""
def _retrieve_dpat_versions(self) -> ApplicationInfo:
"""Retrieves the versions of DPAT from the server."""

versions = ApplicationInfo(**cast(dict, self._get("/info")))
self._headers.update(
Expand All @@ -46,19 +46,19 @@ def _retrieve_ids7_versions(self) -> ApplicationInfo:

@connection_retry()
def _get(self, path: str, **kwargs) -> JSONPayload:
"""Runs a GET request to IDS7. Named args are query parameters."""
"""Runs a GET request to DPAT. Named args are query parameters."""

url = f"{self._url}{path}"
resp = requests.get(url, params=kwargs, headers=self._headers)
if resp.status_code != 200:
raise IDS7RequestError(resp.status_code, resp.text, path)
raise DPATRequestError(resp.status_code, resp.text, path)
return resp.json()

@connection_retry()
def _post(
self, path: str, payload: JSONPayload, parse_response: bool = True
) -> Optional[JSONPayload]:
"""Runs a POST request to IDS7.
"""Runs a POST request to DPAT.
Args:
path (str): Request path
Expand All @@ -72,7 +72,7 @@ def _post(
url = f"{self._url}{path}"
resp = requests.post(url, json=payload, headers=self._headers)
if resp.status_code != 201:
raise IDS7RequestError(resp.status_code, resp.text, path)
raise DPATRequestError(resp.status_code, resp.text, path)
if parse_response:
return resp.json()
return None
Expand All @@ -81,7 +81,7 @@ def _post(
def _put(
self, path: str, values: JSONPayload, parse_response: bool = True
) -> Optional[JSONPayload]:
"""Runs a PUT request to IDS7.
"""Runs a PUT request to DPAT.
Args:
path (str): Request path
Expand All @@ -95,7 +95,7 @@ def _put(
url = f"{self._url}{path}"
resp = requests.put(url, json=values, headers=self._headers)
if resp.status_code != 200:
raise IDS7RequestError(resp.status_code, resp.text, path)
raise DPATRequestError(resp.status_code, resp.text, path)
if parse_response:
return resp.json()
return None
Expand Down Expand Up @@ -124,13 +124,13 @@ def get_image_info(
return ImageInfo(**cast(dict, self._get(path, **params)))

def create_results(self, results: Result) -> ResultResponse:
"""Creates a result in IDS7.
"""Creates a result in DPAT.
Args:
results (Result): Results payload
Returns:
ResultResponse: Parsed IDS7 response.
ResultResponse: Parsed DPAT response.
"""
path = f"/applications/{self._app_id}/results"
resp = self._post(path, results.model_dump())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class InvocationBase(BaseModel):


class Invocation(InvocationBase):
"""Model for invocations from IDS7."""
"""Model for invocations from DPAT."""

action: Action
input: Optional[Union[CreateInput, ResultResponse]] = None
Expand All @@ -74,6 +74,6 @@ def validate_input(self) -> "Invocation":


class ImageNotification(InvocationBase):
"""Model for new image notification from IDS7."""
"""Model for new image notification from DPAT."""

imageInfo: ImageInfo
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class Attachment(BaseModel):


class Result(BaseModel):
"""Schema for result posting in IDS7 server."""
"""Schema for result posting in DPAT server."""

slideId: str
displayResult: str
Expand All @@ -116,7 +116,7 @@ class Result(BaseModel):


class ResultResponse(Result):
"""Schema for result retrieval from IDS7 server."""
"""Schema for result retrieval from DPAT server."""

id: int
versionId: str
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class IDS7RequestError(Exception):
"""Exception raised when a request to IDS7 has failed."""
class DPATRequestError(Exception):
"""Exception raised when a request to DPAT has failed."""

__slots__ = ("status_code", "text", "path")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def _request(*args, **kwargs):
except ConnectionError:
delay = 2**trial
logger.warning(
"IDS7 connection error trial %s/5, retrying in %ss",
"DPAT connection error trial %s/5, retrying in %ss",
trial,
delay,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from .client import IDS7QidoClient
from .client import DPATQidoClient
from .schemas import NAMES_TO_DICOM_CODES, DicomCodes, DicomObject, DicomValue
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import requests
from requests.auth import HTTPBasicAuth

from ids7client.errors import IDS7RequestError
from ids7client.helpers import JSONPayload, connection_retry
from sectra_dpat_client.errors import DPATRequestError
from sectra_dpat_client.helpers import JSONPayload, connection_retry

from .schemas import NAMES_TO_DICOM_CODES, DicomObject

Expand All @@ -27,11 +27,11 @@ def _make_query_params(**kwargs) -> Dict[str, Any]:
return params


class IDS7QidoClient:
"""Class managing connection and requests to IDS7 server.
class DPATQidoClient:
"""Class managing connection and requests to DPAT server.
Args:
url (str): URL of the IDS7 Qido API
url (str): URL of the DPAT Qido API
username (str): API username
password (str): API password
"""
Expand All @@ -47,11 +47,11 @@ def __init__(self, url: str, username: str, password: str) -> None:

@connection_retry()
def _get(self, path: str, **kwargs) -> JSONPayload:
"""Runs a GET request to IDS7. Named args are query parameters."""
"""Runs a GET request to DPAT. Named args are query parameters."""

resp = requests.get(path, params=kwargs, auth=self._auth)
if resp.status_code != 200:
raise IDS7RequestError(resp.status_code, resp.text, path)
raise DPATRequestError(resp.status_code, resp.text, path)
return resp.json()

def find_all_studies(self, **kwargs) -> List[DicomObject]:
Expand All @@ -66,9 +66,9 @@ def find_one_study(self, **kwargs) -> DicomObject:
"""Finds one study matching query.
Raises:
IDS7RequestError: If no study was found.
DPATRequestError: If no study was found.
"""
studies = self.find_all_studies(**kwargs)
if not studies:
raise IDS7RequestError(404, "No study found", "")
raise DPATRequestError(404, "No study found", "")
return studies[0]
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

@unique
class DicomCodes(str, Enum):
"""Enum for IDS7 dicom codes."""
"""Enum for DPAT dicom codes."""

PATIENT_ID = "00100020"
EXAM_ID = "00200010"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[isort]
profile = black
known_first_party = ids7client
known_first_party = sectra_dpat_client

[darglint]
strictness = long
Expand Down
Loading

0 comments on commit fb0004c

Please sign in to comment.