Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ab06910
Remove obsolete reference to `dicomweb_client.error` module from `pac…
agharwal Feb 25, 2021
1297dcc
Add `create_chc_uri()` to `uri.py`.
agharwal Feb 25, 2021
9dd6c81
Repackage `create_chc_uri()` under new class `CloudHealthcareDICOMSto…
agharwal Mar 8, 2021
5461284
Update documentation for `CloudHealthcareDICOMStore`.
agharwal Mar 8, 2021
ea9bc22
Add `CloudHealthcareDICOMStore.from_url()`.
agharwal Mar 8, 2021
69204ad
Rename `CloudHealthcareDICOMStore` to `GoogleCloudHealthcare`.
agharwal Mar 8, 2021
81dc4c5
Add tests for `GoogleCloudHealthcare` validators.
agharwal Mar 8, 2021
324f7f8
Merge branch 'master' into feature/gcp-uri
agharwal Mar 8, 2021
e288e90
Cosmetic internal change in `GoogleCloudHealthcare`.
agharwal Mar 8, 2021
29b7547
Rename `from_url()` to `from_string()` in `GoogleCloudHealthcare`.
agharwal Mar 8, 2021
15e8fe4
Rename `GoogleCloudHealthcare` to `GoogleCloudHealthcareURL`.
agharwal Mar 8, 2021
1ea17e5
Merge master.
agharwal Apr 16, 2021
32588e2
Change base class of `GoogleCloudHealthcareURL` from `attr` to `datac…
agharwal Apr 17, 2021
2683abb
Fix `flake8` errors in `uri.py`.
agharwal Apr 17, 2021
21ee604
Add `dataclasses` backport for Python 3.6.
agharwal Apr 17, 2021
f40bdf5
Move `uri.GoogleCloudHealthcareURL` to new module `ext.gcp.uri`.
agharwal May 24, 2021
94eb549
Remove dead code from `tests/test_uri.py`.
agharwal May 24, 2021
0a3e9c6
Disable lint check for unused import.
agharwal May 24, 2021
7fb7857
Copy `session_utils.create_session_from_gcp_credentials()` to `ext.gc…
agharwal May 24, 2021
e92771a
Move dependency check in `session_utils.create_session_from_gcp_crede…
agharwal May 25, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ Pre-build package available at PyPi:

pip install dicomweb-client

Additional dependencies required for extensions compatible with
`Google Cloud Platform (GCP)`_ may be installed as:

.. _Google Cloud Platform (GCP): https://cloud.google.com

.. code-block:: none

pip install dicomweb-client[gcp]

Source code available at Github:

.. code-block:: none
Expand Down
26 changes: 17 additions & 9 deletions docs/package.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,6 @@ dicomweb\_client.cli module
.. autoprogram:: dicomweb_client.cli:_get_parser()
:prog: dicomweb_client

dicomweb\_client.error module
+++++++++++++++++++++++++++++

.. automodule:: dicomweb_client.error
:members:
:undoc-members:
:show-inheritance:

dicomweb\_client.log module
+++++++++++++++++++++++++++

Expand All @@ -53,9 +45,25 @@ dicomweb\_client.session_utils module
:show-inheritance:

dicomweb\_client.uri module
+++++++++++++++++++++++++++++++++++++
+++++++++++++++++++++++++++

.. automodule:: dicomweb_client.uri
:members:
:undoc-members:
:show-inheritance:

dicomweb\_client.ext.gcp.session_utils module
+++++++++++++++++++++++++++++++++++++++++++++

.. automodule:: dicomweb_client.ext.gcp.session_utils
:members:
:undoc-members:
:show-inheritance:

dicomweb\_client.ext.gcp.uri module
+++++++++++++++++++++++++++++++++++

.. automodule:: dicomweb_client.ext.gcp.uri
:members:
:undoc-members:
:show-inheritance:
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
package_dir={'': 'src'},
extras_require={
'gcp': [
'dataclasses>=0.8; python_version=="3.6"',
'google-auth>=1.6',
'google-oauth>=1.0',
],
Expand Down
1 change: 1 addition & 0 deletions src/dicomweb_client/ext/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Vendor-specific extensions of the `dicomweb_client` package."""
10 changes: 10 additions & 0 deletions src/dicomweb_client/ext/gcp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Google Cloud Platform (GCP) compatible extensions of `dicomweb_client`.

Modules under this package may require additional dependencies. Instructions for
installation are available in the Installation Guide here:
https://dicomweb-client.readthedocs.io/en/latest/installation.html#installation-guide

For further details about GCP, visit: https://cloud.google.com
"""

from dicomweb_client.ext.gcp.uri import GoogleCloudHealthcareURL # noqa
37 changes: 37 additions & 0 deletions src/dicomweb_client/ext/gcp/session_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Session management utilities for Google Cloud Platform (GCP)."""
from typing import Optional, Any

try:
import google.auth
from google.auth.transport import requests as google_requests
except ImportError:
raise ImportError(
'The `dicomweb-client` package needs to be installed with the '
'"gcp" extra requirements to use this module, as follows: '
'`pip install dicomweb-client[gcp]`')
import requests


def create_session_from_gcp_credentials(
google_credentials: Optional[Any] = None
) -> requests.Session:
"""Creates an authorized session for Google Cloud Platform.

Parameters
----------
google_credentials: Any
Google Cloud credentials.
(see https://cloud.google.com/docs/authentication/production
for more information on Google Cloud authentication).
If not set, will be initialized to ``google.auth.default()``.

Returns
-------
requests.Session
Google Cloud authorized session.
"""
if google_credentials is None:
google_credentials, _ = google.auth.default(
scopes=['https://www.googleapis.com/auth/cloud-platform']
)
return google_requests.AuthorizedSession(google_credentials)
113 changes: 113 additions & 0 deletions src/dicomweb_client/ext/gcp/uri.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""Utilities for Google Cloud Healthcare DICOMweb API URI manipulation.

For details, visit: https://cloud.google.com/healthcare
"""
import dataclasses
import re


# Used for Project ID and Location validation in `GoogleCloudHealthcareURL`.
_REGEX_ID_1 = re.compile(r'[\w-]+')
# Used for Dataset ID and DICOM Store ID validation in
# `GoogleCloudHealthcareURL`.
_REGEX_ID_2 = re.compile(r'[\w.-]+')
# Regex for the DICOM Store suffix for the Google Cloud Healthcare API endpoint.
_STORE_REGEX = re.compile(
(r'projects/(%s)/locations/(%s)/datasets/(%s)/'
r'dicomStores/(%s)/dicomWeb$') % (_REGEX_ID_1.pattern,
_REGEX_ID_1.pattern,
_REGEX_ID_2.pattern,
_REGEX_ID_2.pattern))
# The URL for the Google Cloud Healthcare API endpoint.
_CHC_API_URL = 'https://healthcare.googleapis.com/v1'
# GCP resource name validation error.
_GCP_RESOURCE_ERROR_TMPL = ('`{attribute}` must match regex {regex}. Actual '
'value: {value!r}')


@dataclasses.dataclass(eq=True, frozen=True)
class GoogleCloudHealthcareURL:
"""Base URL container for DICOM Stores under the `Google Cloud Healthcare API`_.

This class facilitates the parsing and creation of :py:attr:`URI.base_url`
corresponding to DICOMweb API Service URLs under the v1_ API. The URLs are
of the form:
``https://healthcare.googleapis.com/v1/projects/{project_id}/locations/{location}/datasets/{dataset_id}/dicomStores/{dicom_store_id}/dicomWeb``

.. _Google Cloud Healthcare API: https://cloud.google.com/healthcare
.. _v1: https://cloud.google.com/healthcare/docs/how-tos/transition-guide

Attributes:
project_id: str
The ID of the `GCP Project
<https://cloud.google.com/healthcare/docs/concepts/projects-datasets-data-stores#projects>`_
that contains the DICOM Store.
location: str
The `Region name
<https://cloud.google.com/healthcare/docs/concepts/regions>`_ of the
geographic location configured for the Dataset that contains the
DICOM Store.
dataset_id: str
The ID of the `Dataset
<https://cloud.google.com/healthcare/docs/concepts/projects-datasets-data-stores#datasets_and_data_stores>`_
that contains the DICOM Store.
dicom_store_id: str
The ID of the `DICOM Store
<https://cloud.google.com/healthcare/docs/concepts/dicom#dicom_stores>`_.
"""
project_id: str
location: str
dataset_id: str
dicom_store_id: str

def __post_init__(self) -> None:
"""Performs input sanity checks."""
for regex, attribute, value in (
(_REGEX_ID_1, 'project_id', self.project_id),
(_REGEX_ID_1, 'location', self.location),
(_REGEX_ID_2, 'dataset_id', self.dataset_id),
(_REGEX_ID_2, 'dicom_store_id', self.dicom_store_id)):
if regex.fullmatch(value) is None:
raise ValueError(_GCP_RESOURCE_ERROR_TMPL.format(
attribute=attribute, regex=regex, value=value))

def __str__(self) -> str:
"""Returns a string URL for use as :py:attr:`URI.base_url`.

See class docstring for the returned URL format.
"""
return (f'{_CHC_API_URL}/'
f'projects/{self.project_id}/'
f'locations/{self.location}/'
f'datasets/{self.dataset_id}/'
f'dicomStores/{self.dicom_store_id}/dicomWeb')

@classmethod
def from_string(cls, base_url: str) -> 'GoogleCloudHealthcareURL':
"""Creates an instance from ``base_url``.

Parameters
----------
base_url: str
The URL for the DICOMweb API Service endpoint corresponding to a
`CHC API DICOM Store
<https://cloud.google.com/healthcare/docs/concepts/dicom#dicom_stores>`_.
See class docstring for supported formats.

Raises
------
ValueError
If ``base_url`` does not match the specifications in the class
docstring.
"""
if not base_url.startswith(f'{_CHC_API_URL}/'):
raise ValueError('Invalid CHC API v1 URL: {base_url!r}')
resource_suffix = base_url[len(_CHC_API_URL) + 1:]

store_match = _STORE_REGEX.match(resource_suffix)
if store_match is None:
raise ValueError(
'Invalid CHC API v1 DICOM Store name: {resource_suffix!r}')

return cls(store_match.group(1), store_match.group(2),
store_match.group(3), store_match.group(4))
25 changes: 9 additions & 16 deletions src/dicomweb_client/session_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
import os
from typing import Optional, Any
import warnings

import requests

Expand Down Expand Up @@ -128,20 +129,12 @@ def create_session_from_gcp_credentials(
-------
requests.Session
Google cloud authorized session

"""
try:
from google.auth.transport import requests as google_requests
if google_credentials is None:
import google.auth
google_credentials, _ = google.auth.default(
scopes=['https://www.googleapis.com/auth/cloud-platform']
)
except ImportError:
raise ImportError(
'The dicomweb-client package needs to be installed with the '
'"gcp" extra requirements to support interaction with the '
'Google Cloud Healthcare API: pip install dicomweb-client[gcp]'
)
logger.debug('initialize, authenticate and authorize HTTP session')
return google_requests.AuthorizedSession(google_credentials)
warnings.warn(
'This method shall be deprecated in a future release. Prefer using the '
'underlying implementation directly, now moved to '
'`dicomweb_client.ext.gcp.session_utils`.',
DeprecationWarning)
import dicomweb_client.ext.gcp.session_utils as gcp_session_utils
return gcp_session_utils.create_session_from_gcp_credentials(
google_credentials)
78 changes: 78 additions & 0 deletions tests/test_ext_gcp_uri.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Unit tests for `dicomweb_client.ext.gcp.uri` module."""
from dicomweb_client.ext.gcp.uri import GoogleCloudHealthcareURL

import pytest

_PROJECT_ID = 'my-project44'
_LOCATION = 'us-central1'
_DATASET_ID = 'my-44.dataset'
_DICOM_STORE_ID = 'my.d1com_store'
_CHC_API_URL = 'https://healthcare.googleapis.com/v1'
_CHC_BASE_URL = (
f'{_CHC_API_URL}/'
f'projects/{_PROJECT_ID}/locations/{_LOCATION}/'
f'datasets/{_DATASET_ID}/dicomStores/{_DICOM_STORE_ID}/dicomWeb')


def test_chc_dicom_store_str():
"""Locks down `GoogleCloudHealthcareURL.__str__()`."""
assert str(
GoogleCloudHealthcareURL(
_PROJECT_ID,
_LOCATION,
_DATASET_ID,
_DICOM_STORE_ID)) == _CHC_BASE_URL


@pytest.mark.parametrize('name', ['hmmm.1', '#95', '43/'])
def test_chc_invalid_project_or_location(name):
"""Tests for bad `project_id`, `location`."""
with pytest.raises(ValueError, match='project_id'):
GoogleCloudHealthcareURL(name, _LOCATION, _DATASET_ID, _DICOM_STORE_ID)
with pytest.raises(ValueError, match='location'):
GoogleCloudHealthcareURL(
_PROJECT_ID, name, _DATASET_ID, _DICOM_STORE_ID)


@pytest.mark.parametrize('name', ['hmmm.!', '#95', '43/'])
def test_chc_invalid_dataset_or_store(name):
"""Tests for bad `dataset_id`, `dicom_store_id`."""
with pytest.raises(ValueError, match='dataset_id'):
GoogleCloudHealthcareURL(_PROJECT_ID, _LOCATION, name, _DICOM_STORE_ID)
with pytest.raises(ValueError, match='dicom_store_id'):
GoogleCloudHealthcareURL(
_PROJECT_ID, _LOCATION, _DATASET_ID, name)


@pytest.mark.parametrize('url', [f'{_CHC_API_URL}beta', 'https://some.url'])
def test_chc_from_string_invalid_api(url):
"""Tests for bad API URL error`GoogleCloudHealthcareURL.from_string()`."""
with pytest.raises(ValueError, match='v1 URL'):
GoogleCloudHealthcareURL.from_string(url)


@pytest.mark.parametrize('url', [
f'{_CHC_BASE_URL}/', # Trailing slash disallowed.
f'{_CHC_API_URL}/project/p/locations/l/datasets/d/dicomStores/ds/dicomWeb',
f'{_CHC_API_URL}/projects/p/location/l/datasets/d/dicomStores/ds/dicomWeb',
f'{_CHC_API_URL}/projects/p/locations/l/dataset/d/dicomStores/ds/dicomWeb',
f'{_CHC_API_URL}/projects/p/locations/l/datasets/d/dicomStore/ds/dicomWeb',
f'{_CHC_API_URL}/locations/l/datasets/d/dicomStores/ds/dicomWeb',
f'{_CHC_API_URL}/projects/p/datasets/d/dicomStores/ds/dicomWeb',
f'{_CHC_API_URL}/projects/p/locations/l/dicomStores/ds/dicomWeb',
f'{_CHC_API_URL}/projects/p/locations/l/datasets/d/dicomWeb',
f'{_CHC_API_URL}/projects/p/locations/l//datasets/d/dicomStores/ds/dicomWeb'
])
def test_chc_from_string_invalid_store_name(url):
"""Tests for bad Store name `GoogleCloudHealthcareURL.from_string()`."""
with pytest.raises(ValueError, match='v1 DICOM'):
GoogleCloudHealthcareURL.from_string(url)


def test_chc_from_string_success():
"""Locks down `GoogleCloudHealthcareURL.from_string()`."""
store = GoogleCloudHealthcareURL.from_string(_CHC_BASE_URL)
assert store.project_id == _PROJECT_ID
assert store.location == _LOCATION
assert store.dataset_id == _DATASET_ID
assert store.dicom_store_id == _DICOM_STORE_ID