Skip to content

Commit 9af7efa

Browse files
committed
Add NHERI-Published and NEES storage support, fix project PRJ resolution
- Add path translation for NHERI-Published (designsafe.storage.published) and NEES (nees.public) in both to_uri and to_path - Fix project resolution: use DesignSafe projects API (/api/projects/v2/) instead of broken Tapis system description search to resolve PRJ numbers to project UUIDs - Add examples/files.ipynb demonstrating file access across all storage systems - Add tests for new storage systems
1 parent 873a410 commit 9af7efa

4 files changed

Lines changed: 363 additions & 81 deletions

File tree

dapi/files.py

Lines changed: 100 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
import os
33
import urllib.parse
44

5-
# No JWT needed if we rely on t.username
6-
# import jwt
5+
import requests
76
from tapipy.tapis import Tapis
87
from tapipy.errors import BaseTapyException
98
from .exceptions import FileOperationError, AuthenticationError
@@ -131,6 +130,16 @@ def tapis_uri_to_local_path(tapis_uri: str) -> str:
131130
else "/home/jupyter/CommunityData/"
132131
)
133132

133+
elif system_id == "designsafe.storage.published":
134+
return (
135+
f"/home/jupyter/NHERI-Published/{path}"
136+
if path
137+
else "/home/jupyter/NHERI-Published/"
138+
)
139+
140+
elif system_id == "nees.public":
141+
return f"/home/jupyter/NEES/{path}" if path else "/home/jupyter/NEES/"
142+
134143
elif system_id.startswith("project-"):
135144
# For Projects: tapis://project-*/path -> /home/jupyter/MyProjects/path
136145
return (
@@ -148,6 +157,48 @@ def tapis_uri_to_local_path(tapis_uri: str) -> str:
148157
return tapis_uri
149158

150159

160+
def _resolve_project_uuid(t: Tapis, project_id: str) -> str:
161+
"""Resolve a DesignSafe project ID (e.g., PRJ-1305) to its Tapis system UUID.
162+
163+
Queries the DesignSafe projects API to find the project UUID, then
164+
returns the corresponding Tapis system ID.
165+
166+
Args:
167+
t (Tapis): Authenticated Tapis client instance (used for the access token).
168+
project_id (str): The DesignSafe project ID (e.g., "PRJ-1305").
169+
170+
Returns:
171+
str: The Tapis system ID (e.g., "project-7997906542076432871-242ac11c-0001-012").
172+
173+
Raises:
174+
FileOperationError: If the project cannot be found or the API request fails.
175+
"""
176+
token = t.access_token.access_token
177+
headers = {"X-Tapis-Token": token, "Authorization": f"Bearer {token}"}
178+
try:
179+
resp = requests.get(
180+
"https://designsafe-ci.org/api/projects/v2/",
181+
headers=headers,
182+
params={"limit": 100},
183+
timeout=30,
184+
)
185+
resp.raise_for_status()
186+
projects = resp.json().get("result", [])
187+
for p in projects:
188+
val = p.get("value", {})
189+
if val.get("projectId", "") == project_id:
190+
uuid = p["uuid"]
191+
return f"project-{uuid}"
192+
except requests.RequestException as e:
193+
raise FileOperationError(
194+
f"Failed to query DesignSafe projects API for '{project_id}': {e}"
195+
) from e
196+
197+
raise FileOperationError(
198+
f"Project '{project_id}' not found. Ensure you have access to this project."
199+
)
200+
201+
151202
def get_ds_path_uri(t: Tapis, path: str, verify_exists: bool = False) -> str:
152203
"""Translate DesignSafe-style paths to Tapis URIs.
153204
@@ -242,16 +293,44 @@ def get_ds_path_uri(t: Tapis, path: str, verify_exists: bool = False) -> str:
242293
print(f"Translated '{path}' to '{input_uri}'")
243294
break # Found match, exit loop
244295

245-
# 3. Handle Project variations (if not already matched)
296+
# 3. Handle NHERI-Published variations (if not already matched)
297+
if input_uri is None:
298+
published_patterns = [
299+
("jupyter/NHERI-Published", "designsafe.storage.published", False),
300+
("/NHERI-Published", "designsafe.storage.published", False),
301+
("NHERI-Published", "designsafe.storage.published", False),
302+
]
303+
for pattern, storage_system_id, use_username in published_patterns:
304+
if pattern in path:
305+
path_remainder = path.split(pattern, 1)[1].lstrip("/")
306+
input_uri = f"tapis://{storage_system_id}/{path_remainder}"
307+
print(f"Translated '{path}' to '{input_uri}'")
308+
break
309+
310+
# 4. Handle NEES variations (if not already matched)
311+
if input_uri is None:
312+
nees_patterns = [
313+
("jupyter/NEES", "nees.public", False),
314+
("/NEES", "nees.public", False),
315+
("NEES", "nees.public", False),
316+
]
317+
for pattern, storage_system_id, use_username in nees_patterns:
318+
if pattern in path:
319+
path_remainder = path.split(pattern, 1)[1].lstrip("/")
320+
input_uri = f"tapis://{storage_system_id}/{path_remainder}"
321+
print(f"Translated '{path}' to '{input_uri}'")
322+
break
323+
324+
# 5. Handle Project variations (if not already matched)
246325
if input_uri is None:
247326
project_patterns = [
248-
("jupyter/MyProjects", "project-"),
249-
("jupyter/projects", "project-"),
250-
("/projects", "project-"),
251-
("projects", "project-"),
252-
("/MyProjects", "project-"),
327+
"jupyter/MyProjects",
328+
"jupyter/projects",
329+
"/projects",
330+
"projects",
331+
"/MyProjects",
253332
]
254-
for pattern, system_prefix in project_patterns:
333+
for pattern in project_patterns:
255334
if pattern in path:
256335
path_remainder_full = path.split(pattern, 1)[1].lstrip("/")
257336
if not path_remainder_full:
@@ -262,73 +341,22 @@ def get_ds_path_uri(t: Tapis, path: str, verify_exists: bool = False) -> str:
262341
project_id_part = parts[0]
263342
path_within_project = parts[1] if len(parts) > 1 else ""
264343

265-
print(f"Searching Tapis systems for project ID '{project_id_part}'...")
266-
found_system_id = None
267-
try:
268-
search_query = (
269-
f"description.like.%{project_id_part}%&id.like.{system_prefix}*"
270-
)
271-
systems = t.systems.getSystems(
272-
search=search_query,
273-
listType="ALL",
274-
select="id,owner,description",
275-
limit=10,
276-
)
277-
matches = []
278-
if systems:
279-
for sys in systems:
280-
if (
281-
project_id_part.lower()
282-
in getattr(sys, "description", "").lower()
283-
):
284-
matches.append(sys.id)
285-
if len(matches) == 1:
286-
found_system_id = matches[0]
287-
print(f"Found unique matching system: {found_system_id}")
288-
elif len(matches) == 0:
289-
if "-" in project_id_part and len(project_id_part) > 30:
290-
potential_sys_id = f"{system_prefix}{project_id_part}"
291-
print(
292-
f"Search failed, attempting direct lookup for system ID: {potential_sys_id}"
293-
)
294-
try:
295-
t.systems.getSystem(
296-
systemId=potential_sys_id, select="id"
297-
) # Select minimal field
298-
found_system_id = potential_sys_id
299-
print(f"Direct lookup successful: {found_system_id}")
300-
except BaseTapyException:
301-
print(
302-
f"Direct lookup for {potential_sys_id} also failed."
303-
)
304-
raise FileOperationError(
305-
f"No project system found matching ID '{project_id_part}' via Tapis v3 search or direct UUID lookup."
306-
)
307-
else:
308-
raise FileOperationError(
309-
f"No project system found matching ID '{project_id_part}' via Tapis v3 search."
310-
)
311-
else:
344+
# If it looks like a UUID already, use it directly
345+
if "-" in project_id_part and len(project_id_part) > 30:
346+
found_system_id = f"project-{project_id_part}"
347+
try:
348+
t.systems.getSystem(systemId=found_system_id, select="id")
349+
except BaseTapyException:
312350
raise FileOperationError(
313-
f"Multiple project systems found potentially matching ID '{project_id_part}': {matches}. Cannot determine unique system."
351+
f"Project system '{found_system_id}' not found via direct lookup."
314352
)
315-
except BaseTapyException as e:
316-
raise FileOperationError(
317-
f"Tapis API error searching for project system '{project_id_part}': {e}"
318-
) from e
319-
except Exception as e:
320-
raise FileOperationError(
321-
f"Unexpected error searching for project system '{project_id_part}': {e}"
322-
) from e
323-
324-
if not found_system_id:
325-
raise FileOperationError(
326-
f"Could not resolve project ID '{project_id_part}' to a Tapis system ID."
327-
)
353+
else:
354+
# Resolve PRJ number via DesignSafe projects API
355+
found_system_id = _resolve_project_uuid(t, project_id_part)
328356

329357
input_uri = f"tapis://{found_system_id}/{path_within_project}"
330-
print(f"Translated '{path}' to '{input_uri}' using Tapis v3 lookup")
331-
break # Found match, exit loop
358+
print(f"Translated '{path}' to '{input_uri}'")
359+
break
332360

333361
# 4. Handle direct tapis:// URI input (if not already matched)
334362
if input_uri is None and path.startswith("tapis://"):

0 commit comments

Comments
 (0)