Skip to content
This repository was archived by the owner on Sep 9, 2024. It is now read-only.

Commit 44984ca

Browse files
authored
Merge pull request #44 from unity-sds/develop
Develop
2 parents b84709a + 38a859e commit 44984ca

File tree

5 files changed

+177
-29
lines changed

5 files changed

+177
-29
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
--------
99

10+
## [0.1.2] - 2023-06-28
11+
12+
### Added
13+
* added method for retrieving datasets `Collection.datasets` from a collection
14+
### Fixed
15+
* Added some directory slash stripping to ensure no trailing slash when specifying "to_stac" output director
16+
### Changed
17+
* Changed name of package from unity-py to unity-sds-client
18+
1019
## [0.1.1] - 2023-06-27
1120

1221
### Added
@@ -16,9 +25,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1625
* Added capability to add files to published application catalogs
1726
* added dependency on pystac > 1.7.3 to unity-py
1827
* added addition of dataset properties to stac read/write
28+
* Added functionality to download latest available version of the application parameter files stored in the Dockstore [[30](https://github.com/unity-sds/unity-py/issues/30)]
1929
### Fixed
30+
* Added some directory slash stripping to ensure no trailing slash when specifying "to_stac" output director
2031
### Changed
2132
* all assets written out to STAC items are made relative (if they are non-URIs, relative, or exist in the same directory tree of the STAC files)
33+
* Changed name of package from unity-py to unity-sds-client
2234
### Removed
2335
* Removed support for python 3.8
2436

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
2-
name = "unity_py"
3-
version = "0.1.1"
2+
name = "unity-sds-client"
3+
version = "0.1.2"
44
description = "Unity-Py is a Python client to simplify interactions with NASA's Unity Platform."
55
authors = ["Anil Natha, Mike Gangl"]
66
readme = "README.md"

tests/test_unity_stac.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def test_read_corrupt_stac():
2020
def test_read_stac():
2121
collection = Collection.from_stac("tests/test_files/cmr_granules.json")
2222
assert collection.collection_id == "C2011289787-GES_DISC"
23-
datasets = collection._datasets
23+
datasets = collection.datasets
2424
assert len(datasets) == 2
2525

2626
data_files = collection.data_locations()
@@ -36,7 +36,7 @@ def test_read_stac():
3636

3737
#Try a "classic" catalog + item files stac catalog
3838
collection = Collection.from_stac("tests/test_files/catalog_01.json")
39-
datasets = collection._datasets
39+
datasets = collection.datasets
4040
assert len(datasets) == 1
4141
data_files = collection.data_locations()
4242
assert len(data_files) == 2

unity_py/resources/collection.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@ def __init__(self, id):
3131
def add_dataset(self, dataset: Dataset):
3232
self._datasets.append(dataset)
3333

34+
@property
35+
def datasets(self):
36+
"""
37+
A method to return the included datasets from a collection object.
38+
39+
Returns
40+
-------
41+
dataset
42+
List of dataset objects
43+
"""
44+
return self._datasets
45+
3446
def data_locations(self, type=[]):
3547
"""
3648
A method to list all asset locations (data, metdata, etc)
@@ -66,6 +78,9 @@ def to_stac(collection, data_dir):
6678
The location of the stac file to read.
6779
6880
"""
81+
# check data dir for a dangling "/"
82+
data_dir = data_dir.rstrip('/')
83+
6984
catalog = Catalog(id=collection.collection_id, description="STAC Catalog")
7085
for dataset in collection._datasets:
7186
updated = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')

unity_py/services/application_service.py

Lines changed: 146 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
import os
33
import requests
4+
from zipfile import ZipFile
45

56
from functools import cached_property
67

@@ -160,28 +161,75 @@ def __init__(self, api_url, token):
160161
"""
161162
Creates a new DockstoreAppCatalog.
162163
163-
api_url: Dockstore API URL
164-
token: Token is a string that can be obtained from the Dockstore user Account screen
164+
Args:
165+
api_url: Dockstore API URL
166+
token: Token is a string that can be obtained from the Dockstore user Account screen
165167
"""
166168

167169
self.api_url = api_url
168170
self.token = token
169171

170172
@property
171173
def _headers(self):
172-
"Headers needed by the Dockstore API"
173-
174+
"""
175+
Headers needed by the Dockstore API.
176+
"""
174177
return {
175178
"accept": "application/json",
176179
"Authorization": f"Bearer {self.token}",
177180
"Content-Type": "application/json",
178181
}
179182

180-
def _get(self, request_url):
183+
@property
184+
def _zip_headers(self):
185+
"""
186+
Header used to download ZIP archive of the workflow descriptor and parameter files.
187+
"""
188+
return {
189+
"Accept": "application/zip",
190+
"Authorization": f"Bearer {self.token}"
191+
}
192+
193+
def _get(self, request_url, params=None):
194+
"""
195+
Submit GET request to the Dockstore API.
196+
197+
Args:
198+
request_url: String representing request URL
199+
params: Optional parameters dictionary for the request. Defaults to None.
200+
201+
Raises:
202+
ApplicationCatalogAccessError: unexpected status code: XXX with message: YYY
203+
204+
Returns:
205+
requests.Response
206+
"""
181207

182208
request_url = request_url.strip("/")
183209

184-
response = requests.get(f"{self.api_url}/{request_url}", headers=self._headers)
210+
response = requests.get(f"{self.api_url}/{request_url}", headers=self._headers, params=params)
211+
212+
if response.status_code != 200:
213+
raise ApplicationCatalogAccessError(f"GET operation to application catalog at {self.api_url}/{request_url} return unexpected status code: {response.status_code} with message: {response.content}")
214+
215+
return response
216+
217+
def _get_zip(self, request_url):
218+
"""
219+
Submit GET request to the Dockstore API to download ZIP archive of the workflow descriptor and parameter files.
220+
221+
Args:
222+
request_url: String representing request URL
223+
224+
Raises:
225+
ApplicationCatalogAccessError: unexpected status code: XXX with message: YYY
226+
227+
Returns:
228+
requests.Response
229+
"""
230+
request_url = request_url.strip("/")
231+
232+
response = requests.get(f"{self.api_url}/{request_url}", headers=self._zip_headers)
185233

186234
if response.status_code != 200:
187235
raise ApplicationCatalogAccessError(f"GET operation to application catalog at {self.api_url}/{request_url} return unexpected status code: {response.status_code} with message: {response.content}")
@@ -268,11 +316,12 @@ def _file_to_json(file_path: str, dockstore_path: str, file_format: str):
268316
"""
269317
Generate JSON format of the file representation for the Dockstore request.
270318
271-
file_path: Path to the file to create JSON format request representation for.
272-
If None or empty filepath is provided, then "dockstore_path" file will be
273-
removed from the hosted workflow.
274-
dockstore_path: Path to the file in the Dockstore.
275-
file_format: Dockstore file type for the file.
319+
Args:
320+
file_path: Path to the file to create JSON format request representation for.
321+
If None or empty filepath is provided, then "dockstore_path" file will be
322+
removed from the hosted workflow.
323+
dockstore_path: Path to the file in the Dockstore.
324+
file_format: Dockstore file type for the file.
276325
"""
277326
# Dockstore requires absolute path for the file to be uploaded
278327
dockstore_file_path = f'/{dockstore_path}' if dockstore_path[0] != '/' else dockstore_path
@@ -294,6 +343,9 @@ def _file_to_json(file_path: str, dockstore_path: str, file_format: str):
294343
def application(self, app_id: int):
295344
"""
296345
Get application information from the Dockstore based on the application ID.
346+
347+
Args:
348+
app_id: Application ID.
297349
"""
298350
request_url = f"/workflows/{app_id}"
299351
return self._application_from_json(self._get(request_url).json())
@@ -303,7 +355,7 @@ def application_list(self, for_user: bool = False, published: bool = None):
303355
For Dockstore optionally filter the application list for the user belonging to the token
304356
as well as restrict to just published applications.
305357
306-
Unpublished applications can only be seen when using for_user=True
358+
Unpublished applications can only be seen when using for_user=True.
307359
"""
308360
request_url = "/workflows/published"
309361

@@ -350,20 +402,19 @@ def register(
350402
'local_path/params_one.json': 'l1_params/params_one.json'
351403
}
352404
353-
Inputs:
354-
app_name: Application name to register within the Dockstore.
355-
app_type: Type of the application. Default is 'CWL'.
356-
cwl_files: List of CWL format parameter file paths to upload to the Dockstore. Default is an empty list.
357-
json_files: List of JSON format parameter file paths to upload to the Dockstore. Default is an empty list.
358-
filename_map: Mapping of parameter filenames on local file system vs. filename path as to appear in
359-
the Dockstore once the file is uploaded. Default is an empty map meaning that each file will be uploaded into
360-
the Dockstore using its basename.
361-
publish: Flag if registered application should be published within the Dockstore. Default is True meaning that
362-
application should be published once it's registered within the Dockstore. Applications that does not have
363-
any files uploaded to the Dockstore can't be published.
405+
Args:
406+
app_name: Application name to register within the Dockstore.
407+
app_type: Type of the application. Default is 'CWL'.
408+
cwl_files: List of CWL format parameter file paths to upload to the Dockstore. Default is an empty list.
409+
json_files: List of JSON format parameter file paths to upload to the Dockstore. Default is an empty list.
410+
filename_map: Mapping of parameter filenames on local file system vs. filename path as to appear in
411+
the Dockstore once the file is uploaded. Default is an empty map meaning that each file will be uploaded into
412+
the Dockstore using its basename.
413+
publish: Flag if registered application should be published within the Dockstore. Default is True meaning that
414+
application should be published once it's registered within the Dockstore. Applications that does not have
415+
any files uploaded to the Dockstore can't be published.
364416
"""
365-
# Set up request parameters for the Dockstore application as expected for
366-
# the hosted workflow
417+
# Set up request parameters for the Dockstore application as expected for the hosted workflow
367418
params = {
368419
'name': app_name,
369420
'descriptorType': app_type,
@@ -499,6 +550,76 @@ def upload_json_file(self, application, param_filename: str, dockstore_filename:
499550

500551
self._patch(request_url, params)
501552

553+
def get_application_version_info(self, application):
554+
"""
555+
Retrieve version information for the workflow. Generated version information is
556+
a dictionary of the "db_version_id: workflow_version_id" content.
557+
This method identifies a mapping between database ID and the "name" of the workflow version
558+
as it appears in the Dockstore UI.
559+
Docktore uses DB version ID instead of the version ID as it appears in the Dockstore UI for the
560+
file retrieval.
561+
562+
Args:
563+
application: DockstoreApplicationPackage object to retrieve version information for.
564+
565+
Returns:
566+
dict
567+
"""
568+
request_url = f"/workflows/{application.id}"
569+
570+
params = {
571+
'include': 'versions'
572+
}
573+
574+
response = self._get(request_url, params=params).json()
575+
576+
application_versions = {}
577+
for each in response['workflowVersions']:
578+
application_versions[each['id']] = each['name']
579+
580+
return application_versions
581+
582+
def download_files(self, application, output_dir_path: str):
583+
"""
584+
Download latest version of parameter files for the workflow.
585+
The method stores ZIP archive of all parameter files as well as
586+
all extracted files to the "output_dir_path" directory.
587+
588+
Args:
589+
application: DockstoreApplicationPackage to download parameter files for.
590+
output_dir_path: Target directory for the workflow ZIP archive and extracted
591+
workflow files.
592+
"""
593+
app_versions = self.get_application_version_info(application)
594+
595+
# Pick the latest (maximum) workflow version - use DB ID for the version
596+
latest_version_id = max(app_versions.keys())
597+
598+
# Create directory to store workflow files to if it does not exist.
599+
if not os.path.exists(output_dir_path):
600+
os.mkdir(output_dir_path)
601+
602+
# Create ZIP filename with the version name as it appears in the Dockstore UI
603+
zip_file_path = os.path.join(
604+
output_dir_path,
605+
f'application_id{application.id}_v{app_versions[latest_version_id]}.zip'
606+
)
607+
608+
# Download the zip archive file
609+
with open(zip_file_path, 'wb') as f:
610+
request_url = f"/workflows/{application.id}/zip/{latest_version_id}"
611+
response = self._get_zip(request_url)
612+
613+
for chunk in response.iter_content(chunk_size=512):
614+
if chunk: # filter out keep-alive new chunks
615+
f.write(chunk)
616+
617+
# Retrieve files from downloaded ZIP archive
618+
with ZipFile(zip_file_path) as fh:
619+
fh.extractall(output_dir_path)
620+
621+
# Keep the ZIP archive in case it's needed
622+
502623
def publish(self, application):
503624
"""
504625
Publish the workflow.

0 commit comments

Comments
 (0)