11import logging
2+ import warnings
23from collections .abc import Mapping
34from pathlib import Path , PurePosixPath , PureWindowsPath
45
56from tqdm import tqdm
67
7- from . import errors , s3
8+ from . import errors
89from .declare import EXTERNAL_TABLE_ROOT
910from .errors import DataJointError , MissingExternalFile
1011from .hash import uuid_from_buffer , uuid_from_file
1112from .heading import Heading
1213from .settings import config
14+ from .storage import StorageBackend
1315from .table import FreeTable , Table
1416from .utils import safe_copy , safe_write
1517
@@ -38,7 +40,7 @@ class ExternalTable(Table):
3840 def __init__ (self , connection , store , database ):
3941 self .store = store
4042 self .spec = config .get_store_spec (store )
41- self ._s3 = None
43+ self ._storage = None
4244 self .database = database
4345 self ._connection = connection
4446 self ._heading = Heading (
@@ -52,9 +54,8 @@ def __init__(self, connection, store, database):
5254 self ._support = [self .full_table_name ]
5355 if not self .is_declared :
5456 self .declare ()
55- self ._s3 = None
56- if self .spec ["protocol" ] == "file" and not Path (self .spec ["location" ]).is_dir ():
57- raise FileNotFoundError ("Inaccessible local directory %s" % self .spec ["location" ]) from None
57+ # Initialize storage backend (validates configuration)
58+ _ = self .storage
5859
5960 @property
6061 def definition (self ):
@@ -73,17 +74,32 @@ def definition(self):
7374 def table_name (self ):
7475 return f"{ EXTERNAL_TABLE_ROOT } _{ self .store } "
7576
77+ @property
78+ def storage (self ) -> StorageBackend :
79+ """Get or create the storage backend instance."""
80+ if self ._storage is None :
81+ self ._storage = StorageBackend (self .spec )
82+ return self ._storage
83+
7684 @property
7785 def s3 (self ):
78- if self ._s3 is None :
79- self ._s3 = s3 .Folder (** self .spec )
80- return self ._s3
86+ """Deprecated: Use storage property instead."""
87+ warnings .warn (
88+ "ExternalTable.s3 is deprecated. Use ExternalTable.storage instead." ,
89+ DeprecationWarning ,
90+ stacklevel = 2 ,
91+ )
92+ # For backward compatibility, return a legacy s3.Folder if needed
93+ from . import s3
94+ if not hasattr (self , "_s3_legacy" ) or self ._s3_legacy is None :
95+ self ._s3_legacy = s3 .Folder (** self .spec )
96+ return self ._s3_legacy
8197
8298 # - low-level operations - private
8399
84100 def _make_external_filepath (self , relative_filepath ):
85101 """resolve the complete external path based on the relative path"""
86- # Strip root
102+ # Strip root for S3 paths
87103 if self .spec ["protocol" ] == "s3" :
88104 posix_path = PurePosixPath (PureWindowsPath (self .spec ["location" ]))
89105 location_path = (
@@ -92,11 +108,13 @@ def _make_external_filepath(self, relative_filepath):
92108 else Path (posix_path )
93109 )
94110 return PurePosixPath (location_path , relative_filepath )
95- # Preserve root
111+ # Preserve root for local filesystem
96112 elif self .spec ["protocol" ] == "file" :
97113 return PurePosixPath (Path (self .spec ["location" ]), relative_filepath )
98114 else :
99- assert False
115+ # For other protocols (gcs, azure, etc.), treat like S3
116+ location = self .spec .get ("location" , "" )
117+ return PurePosixPath (location , relative_filepath ) if location else PurePosixPath (relative_filepath )
100118
101119 def _make_uuid_path (self , uuid , suffix = "" ):
102120 """create external path based on the uuid hash"""
@@ -109,57 +127,32 @@ def _make_uuid_path(self, uuid, suffix=""):
109127 )
110128
111129 def _upload_file (self , local_path , external_path , metadata = None ):
112- if self .spec ["protocol" ] == "s3" :
113- self .s3 .fput (local_path , external_path , metadata )
114- elif self .spec ["protocol" ] == "file" :
115- safe_copy (local_path , external_path , overwrite = True )
116- else :
117- assert False
130+ """Upload a file to external storage using fsspec backend."""
131+ self .storage .put_file (local_path , external_path , metadata )
118132
119133 def _download_file (self , external_path , download_path ):
120- if self .spec ["protocol" ] == "s3" :
121- self .s3 .fget (external_path , download_path )
122- elif self .spec ["protocol" ] == "file" :
123- safe_copy (external_path , download_path )
124- else :
125- assert False
134+ """Download a file from external storage using fsspec backend."""
135+ self .storage .get_file (external_path , download_path )
126136
127137 def _upload_buffer (self , buffer , external_path ):
128- if self .spec ["protocol" ] == "s3" :
129- self .s3 .put (external_path , buffer )
130- elif self .spec ["protocol" ] == "file" :
131- safe_write (external_path , buffer )
132- else :
133- assert False
138+ """Upload bytes to external storage using fsspec backend."""
139+ self .storage .put_buffer (buffer , external_path )
134140
135141 def _download_buffer (self , external_path ):
136- if self .spec ["protocol" ] == "s3" :
137- return self .s3 .get (external_path )
138- if self .spec ["protocol" ] == "file" :
139- try :
140- return Path (external_path ).read_bytes ()
141- except FileNotFoundError :
142- raise errors .MissingExternalFile (f"Missing external file { external_path } " ) from None
143- assert False
142+ """Download bytes from external storage using fsspec backend."""
143+ return self .storage .get_buffer (external_path )
144144
145145 def _remove_external_file (self , external_path ):
146- if self .spec ["protocol" ] == "s3" :
147- self .s3 .remove_object (external_path )
148- elif self .spec ["protocol" ] == "file" :
149- try :
150- Path (external_path ).unlink ()
151- except FileNotFoundError :
152- pass
146+ """Remove a file from external storage using fsspec backend."""
147+ self .storage .remove (external_path )
153148
154149 def exists (self , external_filepath ):
155150 """
151+ Check if an external file is accessible using fsspec backend.
152+
156153 :return: True if the external file is accessible
157154 """
158- if self .spec ["protocol" ] == "s3" :
159- return self .s3 .exists (external_filepath )
160- if self .spec ["protocol" ] == "file" :
161- return Path (external_filepath ).is_file ()
162- assert False
155+ return self .storage .exists (external_filepath )
163156
164157 # --- BLOBS ----
165158
0 commit comments