Skip to content

Commit f40bdf5

Browse files
committed
Move uri.GoogleCloudHealthcareURL to new module ext.gcp.uri.
1 parent 21ee604 commit f40bdf5

File tree

9 files changed

+221
-176
lines changed

9 files changed

+221
-176
lines changed

docs/installation.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ Pre-build package available at PyPi:
2828
2929
pip install dicomweb-client
3030
31+
Additional dependencies required for extensions compatible with
32+
`Google Cloud Platform (GCP)`_ may be installed as:
33+
34+
.. _Google Cloud Platform (GCP): https://cloud.google.com
35+
36+
.. code-block:: none
37+
38+
pip install dicomweb-client[gcp]
39+
3140
Source code available at Github:
3241

3342
.. code-block:: none

docs/package.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,11 @@ dicomweb\_client.uri module
5151
:members:
5252
:undoc-members:
5353
:show-inheritance:
54+
55+
dicomweb\_client.ext.gcp.uri module
56+
+++++++++++++++++++++++++++++++++++++
57+
58+
.. automodule:: dicomweb_client.ext.gcp.uri
59+
:members:
60+
:undoc-members:
61+
:show-inheritance:

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,13 @@
4242
package_dir={'': 'src'},
4343
extras_require={
4444
'gcp': [
45+
'dataclasses>=0.8; python_version=="3.6"',
4546
'google-auth>=1.6',
4647
'google-oauth>=1.0',
4748
],
4849
},
4950
python_requires='>=3.6',
5051
install_requires=[
51-
'dataclasses; python_version=="3.6"',
5252
'pydicom>=2.0',
5353
'requests>=2.18',
5454
'retrying>=1.3.3',
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Vendor-specific extensions of the `dicomweb_client` package."""
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Google Cloud Platform (GCP) compatible extensions of `dicomweb_client`.
2+
3+
Modules under this package may require additional dependencies. Instructions for
4+
installation are available in the Installation Guide here:
5+
https://dicomweb-client.readthedocs.io/en/latest/installation.html#installation-guide
6+
7+
For further details about GCP, visit: https://cloud.google.com
8+
"""
9+
10+
from dicomweb_client.ext.gcp.uri import GoogleCloudHealthcareURL

src/dicomweb_client/ext/gcp/uri.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""Utilities for Google Cloud Healthcare DICOMweb API URI manipulation.
2+
3+
For details, visit: https://cloud.google.com/healthcare
4+
"""
5+
import dataclasses
6+
import re
7+
8+
9+
# Used for Project ID and Location validation in `GoogleCloudHealthcareURL`.
10+
_REGEX_ID_1 = re.compile(r'[\w-]+')
11+
# Used for Dataset ID and DICOM Store ID validation in
12+
# `GoogleCloudHealthcareURL`.
13+
_REGEX_ID_2 = re.compile(r'[\w.-]+')
14+
# Regex for the DICOM Store suffix for the Google Cloud Healthcare API endpoint.
15+
_STORE_REGEX = re.compile(
16+
(r'projects/(%s)/locations/(%s)/datasets/(%s)/'
17+
r'dicomStores/(%s)/dicomWeb$') % (_REGEX_ID_1.pattern,
18+
_REGEX_ID_1.pattern,
19+
_REGEX_ID_2.pattern,
20+
_REGEX_ID_2.pattern))
21+
# The URL for the Google Cloud Healthcare API endpoint.
22+
_CHC_API_URL = 'https://healthcare.googleapis.com/v1'
23+
# GCP resource name validation error.
24+
_GCP_RESOURCE_ERROR_TMPL = ('`{attribute}` must match regex {regex}. Actual '
25+
'value: {value!r}')
26+
27+
28+
@dataclasses.dataclass(eq=True, frozen=True)
29+
class GoogleCloudHealthcareURL:
30+
"""Base URL container for DICOM Stores under the `Google Cloud Healthcare API`_.
31+
32+
This class facilitates the parsing and creation of :py:attr:`URI.base_url`
33+
corresponding to DICOMweb API Service URLs under the v1_ API. The URLs are
34+
of the form:
35+
``https://healthcare.googleapis.com/v1/projects/{project_id}/locations/{location}/datasets/{dataset_id}/dicomStores/{dicom_store_id}/dicomWeb``
36+
37+
.. _Google Cloud Healthcare API: https://cloud.google.com/healthcare
38+
.. _v1: https://cloud.google.com/healthcare/docs/how-tos/transition-guide
39+
40+
Attributes:
41+
project_id: str
42+
The ID of the `GCP Project
43+
<https://cloud.google.com/healthcare/docs/concepts/projects-datasets-data-stores#projects>`_
44+
that contains the DICOM Store.
45+
location: str
46+
The `Region name
47+
<https://cloud.google.com/healthcare/docs/concepts/regions>`_ of the
48+
geographic location configured for the Dataset that contains the
49+
DICOM Store.
50+
dataset_id: str
51+
The ID of the `Dataset
52+
<https://cloud.google.com/healthcare/docs/concepts/projects-datasets-data-stores#datasets_and_data_stores>`_
53+
that contains the DICOM Store.
54+
dicom_store_id: str
55+
The ID of the `DICOM Store
56+
<https://cloud.google.com/healthcare/docs/concepts/dicom#dicom_stores>`_.
57+
"""
58+
project_id: str
59+
location: str
60+
dataset_id: str
61+
dicom_store_id: str
62+
63+
def __post_init__(self) -> None:
64+
"""Performs input sanity checks."""
65+
for regex, attribute, value in (
66+
(_REGEX_ID_1, 'project_id', self.project_id),
67+
(_REGEX_ID_1, 'location', self.location),
68+
(_REGEX_ID_2, 'dataset_id', self.dataset_id),
69+
(_REGEX_ID_2, 'dicom_store_id', self.dicom_store_id)):
70+
if regex.fullmatch(value) is None:
71+
raise ValueError(_GCP_RESOURCE_ERROR_TMPL.format(
72+
attribute=attribute, regex=regex, value=value))
73+
74+
def __str__(self) -> str:
75+
"""Returns a string URL for use as :py:attr:`URI.base_url`.
76+
77+
See class docstring for the returned URL format.
78+
"""
79+
return (f'{_CHC_API_URL}/'
80+
f'projects/{self.project_id}/'
81+
f'locations/{self.location}/'
82+
f'datasets/{self.dataset_id}/'
83+
f'dicomStores/{self.dicom_store_id}/dicomWeb')
84+
85+
@classmethod
86+
def from_string(cls, base_url: str) -> 'GoogleCloudHealthcareURL':
87+
"""Creates an instance from ``base_url``.
88+
89+
Parameters
90+
----------
91+
base_url: str
92+
The URL for the DICOMweb API Service endpoint corresponding to a
93+
`CHC API DICOM Store
94+
<https://cloud.google.com/healthcare/docs/concepts/dicom#dicom_stores>`_.
95+
See class docstring for supported formats.
96+
97+
Raises
98+
------
99+
ValueError
100+
If ``base_url`` does not match the specifications in the class
101+
docstring.
102+
"""
103+
if not base_url.startswith(f'{_CHC_API_URL}/'):
104+
raise ValueError('Invalid CHC API v1 URL: {base_url!r}')
105+
resource_suffix = base_url[len(_CHC_API_URL) + 1:]
106+
107+
store_match = _STORE_REGEX.match(resource_suffix)
108+
if store_match is None:
109+
raise ValueError(
110+
'Invalid CHC API v1 DICOM Store name: {resource_suffix!r}')
111+
112+
return cls(store_match.group(1), store_match.group(2),
113+
store_match.group(3), store_match.group(4))

src/dicomweb_client/uri.py

Lines changed: 0 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
"""Utilities for DICOMweb URI manipulation."""
2-
import dataclasses
32
import enum
43
import re
54
from typing import Optional, Sequence, Tuple
@@ -26,23 +25,6 @@ class URISuffix(enum.Enum):
2625
_MAX_UID_LENGTH = 64
2726
_REGEX_UID = re.compile(r'[0-9]+([.][0-9]+)*')
2827
_REGEX_PERMISSIVE_UID = re.compile(r'[^/@]+')
29-
# Used for Project ID and Location validation in `GoogleCloudHealthcareURL`.
30-
_REGEX_ID_1 = re.compile(r'[\w-]+')
31-
# Used for Dataset ID and DICOM Store ID validation in
32-
# `GoogleCloudHealthcareURL`.
33-
_REGEX_ID_2 = re.compile(r'[\w.-]+')
34-
# Regex for the DICOM Store suffix for the Google Cloud Healthcare API endpoint.
35-
_STORE_REGEX = re.compile(
36-
(r'projects/(%s)/locations/(%s)/datasets/(%s)/'
37-
r'dicomStores/(%s)/dicomWeb$') % (_REGEX_ID_1.pattern,
38-
_REGEX_ID_1.pattern,
39-
_REGEX_ID_2.pattern,
40-
_REGEX_ID_2.pattern))
41-
# The URL for the Google Cloud Healthcare API endpoint.
42-
_CHC_API_URL = 'https://healthcare.googleapis.com/v1'
43-
# Cloud Healthcare validation error.
44-
_CHC_API_ERROR_TMPL = ('`{attribute}` must match regex {regex}. Actual value: '
45-
'{value!r}')
4628

4729

4830
class URI:
@@ -484,94 +466,6 @@ def from_string(cls,
484466
return uri
485467

486468

487-
@dataclasses.dataclass(eq=True, frozen=True)
488-
class GoogleCloudHealthcareURL:
489-
"""Base URL container for DICOM Stores under the `Google Cloud Healthcare API`_.
490-
491-
This class facilitates the parsing and creation of :py:attr:`URI.base_url`
492-
corresponding to DICOMweb API Service URLs under the v1_ API. The URLs are
493-
of the form:
494-
``https://healthcare.googleapis.com/v1/projects/{project_id}/locations/{location}/datasets/{dataset_id}/dicomStores/{dicom_store_id}/dicomWeb``
495-
496-
.. _Google Cloud Healthcare API: https://cloud.google.com/healthcare
497-
.. _v1: https://cloud.google.com/healthcare/docs/how-tos/transition-guide
498-
499-
Attributes:
500-
project_id: str
501-
The ID of the `GCP Project
502-
<https://cloud.google.com/healthcare/docs/concepts/projects-datasets-data-stores#projects>`_
503-
that contains the DICOM Store.
504-
location: str
505-
The `Region name
506-
<https://cloud.google.com/healthcare/docs/concepts/regions>`_ of the
507-
geographic location configured for the Dataset that contains the
508-
DICOM Store.
509-
dataset_id: str
510-
The ID of the `Dataset
511-
<https://cloud.google.com/healthcare/docs/concepts/projects-datasets-data-stores#datasets_and_data_stores>`_
512-
that contains the DICOM Store.
513-
dicom_store_id: str
514-
The ID of the `DICOM Store
515-
<https://cloud.google.com/healthcare/docs/concepts/dicom#dicom_stores>`_.
516-
"""
517-
project_id: str
518-
location: str
519-
dataset_id: str
520-
dicom_store_id: str
521-
522-
def __post_init__(self) -> None:
523-
"""Performs input sanity checks."""
524-
for regex, attribute, value in (
525-
(_REGEX_ID_1, 'project_id', self.project_id),
526-
(_REGEX_ID_1, 'location', self.location),
527-
(_REGEX_ID_2, 'dataset_id', self.dataset_id),
528-
(_REGEX_ID_2, 'dicom_store_id', self.dicom_store_id)):
529-
if regex.fullmatch(value) is None:
530-
raise ValueError(_CHC_API_ERROR_TMPL.format(
531-
attribute=attribute, regex=regex, value=value))
532-
533-
def __str__(self) -> str:
534-
"""Returns a string URL for use as :py:attr:`URI.base_url`.
535-
536-
See class docstring for the returned URL format.
537-
"""
538-
return (f'{_CHC_API_URL}/'
539-
f'projects/{self.project_id}/'
540-
f'locations/{self.location}/'
541-
f'datasets/{self.dataset_id}/'
542-
f'dicomStores/{self.dicom_store_id}/dicomWeb')
543-
544-
@classmethod
545-
def from_string(cls, base_url: str) -> 'GoogleCloudHealthcareURL':
546-
"""Creates an instance from ``base_url``.
547-
548-
Parameters
549-
----------
550-
base_url: str
551-
The URL for the DICOMweb API Service endpoint corresponding to a
552-
`CHC API DICOM Store
553-
<https://cloud.google.com/healthcare/docs/concepts/dicom#dicom_stores>`_.
554-
See class docstring for supported formats.
555-
556-
Raises
557-
------
558-
ValueError
559-
If ``base_url`` does not match the specifications in the class
560-
docstring.
561-
"""
562-
if not base_url.startswith(f'{_CHC_API_URL}/'):
563-
raise ValueError('Invalid CHC API v1 URL: {base_url!r}')
564-
resource_suffix = base_url[len(_CHC_API_URL) + 1:]
565-
566-
store_match = _STORE_REGEX.match(resource_suffix)
567-
if store_match is None:
568-
raise ValueError(
569-
'Invalid CHC API v1 DICOM Store name: {resource_suffix!r}')
570-
571-
return cls(store_match.group(1), store_match.group(2),
572-
store_match.group(3), store_match.group(4))
573-
574-
575469
def _validate_base_url(url: str) -> None:
576470
"""Validates the Base (DICOMweb service) URL supplied to `URI`."""
577471
parse_result = urlparse.urlparse(url)

tests/test_ext_gcp_uri.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Unit tests for `dicomweb_client.ext.gcp.uri` module."""
2+
from dicomweb_client.ext.gcp.uri import GoogleCloudHealthcareURL
3+
4+
import pytest
5+
6+
_PROJECT_ID = 'my-project44'
7+
_LOCATION = 'us-central1'
8+
_DATASET_ID = 'my-44.dataset'
9+
_DICOM_STORE_ID = 'my.d1com_store'
10+
_CHC_API_URL = 'https://healthcare.googleapis.com/v1'
11+
_CHC_BASE_URL = (
12+
f'{_CHC_API_URL}/'
13+
f'projects/{_PROJECT_ID}/locations/{_LOCATION}/'
14+
f'datasets/{_DATASET_ID}/dicomStores/{_DICOM_STORE_ID}/dicomWeb')
15+
16+
17+
def test_chc_dicom_store_str():
18+
"""Locks down `GoogleCloudHealthcareURL.__str__()`."""
19+
assert str(
20+
GoogleCloudHealthcareURL(
21+
_PROJECT_ID,
22+
_LOCATION,
23+
_DATASET_ID,
24+
_DICOM_STORE_ID)) == _CHC_BASE_URL
25+
26+
27+
@pytest.mark.parametrize('name', ['hmmm.1', '#95', '43/'])
28+
def test_chc_invalid_project_or_location(name):
29+
"""Tests for bad `project_id`, `location`."""
30+
with pytest.raises(ValueError, match='project_id'):
31+
GoogleCloudHealthcareURL(name, _LOCATION, _DATASET_ID, _DICOM_STORE_ID)
32+
with pytest.raises(ValueError, match='location'):
33+
GoogleCloudHealthcareURL(
34+
_PROJECT_ID, name, _DATASET_ID, _DICOM_STORE_ID)
35+
36+
37+
@pytest.mark.parametrize('name', ['hmmm.!', '#95', '43/'])
38+
def test_chc_invalid_dataset_or_store(name):
39+
"""Tests for bad `dataset_id`, `dicom_store_id`."""
40+
with pytest.raises(ValueError, match='dataset_id'):
41+
GoogleCloudHealthcareURL(_PROJECT_ID, _LOCATION, name, _DICOM_STORE_ID)
42+
with pytest.raises(ValueError, match='dicom_store_id'):
43+
GoogleCloudHealthcareURL(
44+
_PROJECT_ID, _LOCATION, _DATASET_ID, name)
45+
46+
47+
@pytest.mark.parametrize('url', [f'{_CHC_API_URL}beta', 'https://some.url'])
48+
def test_chc_from_string_invalid_api(url):
49+
"""Tests for bad API URL error`GoogleCloudHealthcareURL.from_string()`."""
50+
with pytest.raises(ValueError, match='v1 URL'):
51+
GoogleCloudHealthcareURL.from_string(url)
52+
53+
54+
@pytest.mark.parametrize('url', [
55+
f'{_CHC_BASE_URL}/', # Trailing slash disallowed.
56+
f'{_CHC_API_URL}/project/p/locations/l/datasets/d/dicomStores/ds/dicomWeb',
57+
f'{_CHC_API_URL}/projects/p/location/l/datasets/d/dicomStores/ds/dicomWeb',
58+
f'{_CHC_API_URL}/projects/p/locations/l/dataset/d/dicomStores/ds/dicomWeb',
59+
f'{_CHC_API_URL}/projects/p/locations/l/datasets/d/dicomStore/ds/dicomWeb',
60+
f'{_CHC_API_URL}/locations/l/datasets/d/dicomStores/ds/dicomWeb',
61+
f'{_CHC_API_URL}/projects/p/datasets/d/dicomStores/ds/dicomWeb',
62+
f'{_CHC_API_URL}/projects/p/locations/l/dicomStores/ds/dicomWeb',
63+
f'{_CHC_API_URL}/projects/p/locations/l/datasets/d/dicomWeb',
64+
f'{_CHC_API_URL}/projects/p/locations/l//datasets/d/dicomStores/ds/dicomWeb'
65+
])
66+
def test_chc_from_string_invalid_store_name(url):
67+
"""Tests for bad Store name `GoogleCloudHealthcareURL.from_string()`."""
68+
with pytest.raises(ValueError, match='v1 DICOM'):
69+
GoogleCloudHealthcareURL.from_string(url)
70+
71+
72+
def test_chc_from_string_success():
73+
"""Locks down `GoogleCloudHealthcareURL.from_string()`."""
74+
store = GoogleCloudHealthcareURL.from_string(_CHC_BASE_URL)
75+
assert store.project_id == _PROJECT_ID
76+
assert store.location == _LOCATION
77+
assert store.dataset_id == _DATASET_ID
78+
assert store.dicom_store_id == _DICOM_STORE_ID

0 commit comments

Comments
 (0)