-
Notifications
You must be signed in to change notification settings - Fork 39
DICOMweb URL manipulation for Google Cloud Healthcare API #48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
hackermd
merged 20 commits into
ImagingDataCommons:master
from
agharwal:feature/gcp-uri
May 25, 2021
Merged
Changes from 19 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 1297dcc
Add `create_chc_uri()` to `uri.py`.
agharwal 9dd6c81
Repackage `create_chc_uri()` under new class `CloudHealthcareDICOMSto…
agharwal 5461284
Update documentation for `CloudHealthcareDICOMStore`.
agharwal ea9bc22
Add `CloudHealthcareDICOMStore.from_url()`.
agharwal 69204ad
Rename `CloudHealthcareDICOMStore` to `GoogleCloudHealthcare`.
agharwal 81dc4c5
Add tests for `GoogleCloudHealthcare` validators.
agharwal 324f7f8
Merge branch 'master' into feature/gcp-uri
agharwal e288e90
Cosmetic internal change in `GoogleCloudHealthcare`.
agharwal 29b7547
Rename `from_url()` to `from_string()` in `GoogleCloudHealthcare`.
agharwal 15e8fe4
Rename `GoogleCloudHealthcare` to `GoogleCloudHealthcareURL`.
agharwal 1ea17e5
Merge master.
agharwal 32588e2
Change base class of `GoogleCloudHealthcareURL` from `attr` to `datac…
agharwal 2683abb
Fix `flake8` errors in `uri.py`.
agharwal 21ee604
Add `dataclasses` backport for Python 3.6.
agharwal f40bdf5
Move `uri.GoogleCloudHealthcareURL` to new module `ext.gcp.uri`.
agharwal 94eb549
Remove dead code from `tests/test_uri.py`.
agharwal 0a3e9c6
Disable lint check for unused import.
agharwal 7fb7857
Copy `session_utils.create_session_from_gcp_credentials()` to `ext.gc…
agharwal e92771a
Move dependency check in `session_utils.create_session_from_gcp_crede…
agharwal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """Vendor-specific extensions of the `dicomweb_client` package.""" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| """Session management utilities for Google Cloud Platform (GCP).""" | ||
| from typing import Optional, Any | ||
|
|
||
| import google.auth | ||
| from google.auth.transport import requests as google_requests | ||
| 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) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.