@@ -126,8 +126,8 @@ class SigMFFile(SigMFMetafile):
126126 COMMENT_KEY = "core:comment"
127127 DESCRIPTION_KEY = "core:description"
128128 AUTHOR_KEY = "core:author"
129- META_DOI_KEY = "core:meta-doi "
130- DATA_DOI_KEY = "core:data-doi "
129+ META_DOI_KEY = "core:meta_doi "
130+ DATA_DOI_KEY = "core:data_doi "
131131 GENERATOR_KEY = "core:generator"
132132 LABEL_KEY = "core:label"
133133 RECORDER_KEY = "core:recorder"
@@ -147,14 +147,38 @@ class SigMFFile(SigMFMetafile):
147147 CAPTURE_KEY = "captures"
148148 ANNOTATION_KEY = "annotations"
149149 VALID_GLOBAL_KEYS = [
150- AUTHOR_KEY , COLLECTION_KEY , DATASET_KEY , DATATYPE_KEY , DATA_DOI_KEY , DESCRIPTION_KEY , EXTENSIONS_KEY ,
151- GEOLOCATION_KEY , HASH_KEY , HW_KEY , LICENSE_KEY , META_DOI_KEY , METADATA_ONLY_KEY , NUM_CHANNELS_KEY , RECORDER_KEY ,
152- SAMPLE_RATE_KEY , START_OFFSET_KEY , TRAILING_BYTES_KEY , VERSION_KEY
150+ AUTHOR_KEY ,
151+ COLLECTION_KEY ,
152+ DATASET_KEY ,
153+ DATATYPE_KEY ,
154+ DATA_DOI_KEY ,
155+ DESCRIPTION_KEY ,
156+ EXTENSIONS_KEY ,
157+ GEOLOCATION_KEY ,
158+ HASH_KEY ,
159+ HW_KEY ,
160+ LICENSE_KEY ,
161+ META_DOI_KEY ,
162+ METADATA_ONLY_KEY ,
163+ NUM_CHANNELS_KEY ,
164+ RECORDER_KEY ,
165+ SAMPLE_RATE_KEY ,
166+ START_OFFSET_KEY ,
167+ TRAILING_BYTES_KEY ,
168+ VERSION_KEY ,
153169 ]
154170 VALID_CAPTURE_KEYS = [DATETIME_KEY , FREQUENCY_KEY , HEADER_BYTES_KEY , GLOBAL_INDEX_KEY , START_INDEX_KEY ]
155171 VALID_ANNOTATION_KEYS = [
156- COMMENT_KEY , FHI_KEY , FLO_KEY , GENERATOR_KEY , LABEL_KEY , LAT_KEY , LENGTH_INDEX_KEY , LON_KEY , START_INDEX_KEY ,
157- UUID_KEY
172+ COMMENT_KEY ,
173+ FHI_KEY ,
174+ FLO_KEY ,
175+ GENERATOR_KEY ,
176+ LABEL_KEY ,
177+ LAT_KEY ,
178+ LENGTH_INDEX_KEY ,
179+ LON_KEY ,
180+ START_INDEX_KEY ,
181+ UUID_KEY ,
158182 ]
159183 VALID_KEYS = {GLOBAL_KEY : VALID_GLOBAL_KEYS , CAPTURE_KEY : VALID_CAPTURE_KEYS , ANNOTATION_KEY : VALID_ANNOTATION_KEYS }
160184
@@ -201,6 +225,75 @@ def __eq__(self, other):
201225 return self ._metadata == other ._metadata
202226 return False
203227
228+ def __getattr__ (self , name ):
229+ """
230+ Enable dynamic attribute access for core global metadata fields.
231+
232+ Allows convenient access to core metadata fields using attribute notation:
233+ - `sigmf_file.sample_rate` returns `sigmf_file._metadata["global"]["core:sample_rate"]
234+ - `sigmf_file.author` returns `sigmf_file._metadata["global"]["core:author"]
235+
236+ Parameters
237+ ----------
238+ name : str
239+ Attribute name corresponding to a core field (without "core:" prefix).
240+
241+ Returns
242+ -------
243+ value
244+ The value of the core field from global metadata, or None if not set.
245+
246+ Raises
247+ ------
248+ SigMFAccessError
249+ If the attribute name doesn't correspond to a valid core global field.
250+ """
251+ # iterate through valid global keys to find matching core field
252+ for key in self .VALID_GLOBAL_KEYS :
253+ if key .startswith ("core:" ) and key [5 :] == name :
254+ field_value = self .get_global_field (key )
255+ if field_value is None :
256+ raise SigMFAccessError (f"Core field '{ key } ' does not exist in global metadata" )
257+ return field_value
258+
259+ # if we get here, the attribute doesn't correspond to a core field
260+ raise AttributeError (f"'{ type (self ).__name__ } ' object has no attribute '{ name } '" )
261+
262+ def __setattr__ (self , name , value ):
263+ """
264+ Enable dynamic attribute setting for core global metadata fields.
265+
266+ Allows convenient setting of core metadata fields using attribute notation:
267+ - `sigmf_file.sample_rate = 1000000` sets `sigmf_file._metadata["global"]["core:sample_rate"]
268+ - `sigmf_file.author = "jane.doe@domain.org"` sets `sigmf_file._metadata["global"]["core:author"]
269+
270+ Parameters
271+ ----------
272+ name : str
273+ Attribute name. If it corresponds to a core field (without "core:" prefix),
274+ the value will be set in global metadata. Otherwise, normal attribute setting occurs.
275+ value
276+ The value to set for the field.
277+ """
278+ # handle regular instance attributes, existing properties, or during initialization
279+ if (
280+ name .startswith ("_" )
281+ or hasattr (type (self ), name )
282+ or not hasattr (self , "_metadata" )
283+ or self ._metadata is None
284+ ):
285+ super ().__setattr__ (name , value )
286+ return
287+
288+ # check if this corresponds to a core global field
289+ for key in self .VALID_GLOBAL_KEYS :
290+ if key .startswith ("core:" ) and key [5 :] == name :
291+ self .set_global_field (key , value )
292+ return
293+
294+ # fall back to normal attribute setting for non-core attributes
295+ super ().__setattr__ (name , value )
296+
204297 def __next__ (self ):
205298 """get next batch of samples"""
206299 if self .iter_position < len (self ):
@@ -782,7 +875,9 @@ class SigMFCollection(SigMFMetafile):
782875 ]
783876 VALID_KEYS = {COLLECTION_KEY : VALID_COLLECTION_KEYS }
784877
785- def __init__ (self , metafiles : list = None , metadata : dict = None , base_path = None , skip_checksums : bool = False ) -> None :
878+ def __init__ (
879+ self , metafiles : list = None , metadata : dict = None , base_path = None , skip_checksums : bool = False
880+ ) -> None :
786881 """
787882 Create a SigMF Collection object.
788883
@@ -1060,6 +1155,7 @@ def fromarchive(archive_path, dir=None, skip_checksum=False):
10601155 access SigMF archives without extracting them.
10611156 """
10621157 from .archivereader import SigMFArchiveReader
1158+
10631159 return SigMFArchiveReader (archive_path , skip_checksum = skip_checksum ).sigmffile
10641160
10651161
@@ -1133,8 +1229,10 @@ def get_sigmf_filenames(filename):
11331229 # suffix, because the filename might contain '.' characters which are part
11341230 # of the filename rather than an extension.
11351231 sigmf_suffixes = [
1136- SIGMF_DATASET_EXT , SIGMF_METADATA_EXT ,
1137- SIGMF_ARCHIVE_EXT , SIGMF_COLLECTION_EXT ,
1232+ SIGMF_DATASET_EXT ,
1233+ SIGMF_METADATA_EXT ,
1234+ SIGMF_ARCHIVE_EXT ,
1235+ SIGMF_COLLECTION_EXT ,
11381236 ]
11391237 if stem_path .suffix in sigmf_suffixes :
11401238 with_suffix_path = stem_path
0 commit comments