22import os
33import urllib .parse
44
5- # No JWT needed if we rely on t.username
6- # import jwt
5+ import requests
76from tapipy .tapis import Tapis
87from tapipy .errors import BaseTapyException
98from .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+
151202def 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