@@ -125,8 +125,8 @@ class SigMFFile(SigMFMetafile):
125125 COMMENT_KEY = "core:comment"
126126 DESCRIPTION_KEY = "core:description"
127127 AUTHOR_KEY = "core:author"
128- META_DOI_KEY = "core:meta-doi "
129- DATA_DOI_KEY = "core:data-doi "
128+ META_DOI_KEY = "core:meta_doi "
129+ DATA_DOI_KEY = "core:data_doi "
130130 GENERATOR_KEY = "core:generator"
131131 LABEL_KEY = "core:label"
132132 RECORDER_KEY = "core:recorder"
@@ -146,14 +146,38 @@ class SigMFFile(SigMFMetafile):
146146 CAPTURE_KEY = "captures"
147147 ANNOTATION_KEY = "annotations"
148148 VALID_GLOBAL_KEYS = [
149- AUTHOR_KEY , COLLECTION_KEY , DATASET_KEY , DATATYPE_KEY , DATA_DOI_KEY , DESCRIPTION_KEY , EXTENSIONS_KEY ,
150- GEOLOCATION_KEY , HASH_KEY , HW_KEY , LICENSE_KEY , META_DOI_KEY , METADATA_ONLY_KEY , NUM_CHANNELS_KEY , RECORDER_KEY ,
151- SAMPLE_RATE_KEY , START_OFFSET_KEY , TRAILING_BYTES_KEY , VERSION_KEY
149+ AUTHOR_KEY ,
150+ COLLECTION_KEY ,
151+ DATASET_KEY ,
152+ DATATYPE_KEY ,
153+ DATA_DOI_KEY ,
154+ DESCRIPTION_KEY ,
155+ EXTENSIONS_KEY ,
156+ GEOLOCATION_KEY ,
157+ HASH_KEY ,
158+ HW_KEY ,
159+ LICENSE_KEY ,
160+ META_DOI_KEY ,
161+ METADATA_ONLY_KEY ,
162+ NUM_CHANNELS_KEY ,
163+ RECORDER_KEY ,
164+ SAMPLE_RATE_KEY ,
165+ START_OFFSET_KEY ,
166+ TRAILING_BYTES_KEY ,
167+ VERSION_KEY ,
152168 ]
153169 VALID_CAPTURE_KEYS = [DATETIME_KEY , FREQUENCY_KEY , HEADER_BYTES_KEY , GLOBAL_INDEX_KEY , START_INDEX_KEY ]
154170 VALID_ANNOTATION_KEYS = [
155- COMMENT_KEY , FHI_KEY , FLO_KEY , GENERATOR_KEY , LABEL_KEY , LAT_KEY , LENGTH_INDEX_KEY , LON_KEY , START_INDEX_KEY ,
156- UUID_KEY
171+ COMMENT_KEY ,
172+ FHI_KEY ,
173+ FLO_KEY ,
174+ GENERATOR_KEY ,
175+ LABEL_KEY ,
176+ LAT_KEY ,
177+ LENGTH_INDEX_KEY ,
178+ LON_KEY ,
179+ START_INDEX_KEY ,
180+ UUID_KEY ,
157181 ]
158182 VALID_KEYS = {GLOBAL_KEY : VALID_GLOBAL_KEYS , CAPTURE_KEY : VALID_CAPTURE_KEYS , ANNOTATION_KEY : VALID_ANNOTATION_KEYS }
159183
@@ -200,6 +224,75 @@ def __eq__(self, other):
200224 return self ._metadata == other ._metadata
201225 return False
202226
227+ def __getattr__ (self , name ):
228+ """
229+ Enable dynamic attribute access for core global metadata fields.
230+
231+ Allows convenient access to core metadata fields using attribute notation:
232+ - `sigmf_file.sample_rate` returns `sigmf_file._metadata["global"]["core:sample_rate"]
233+ - `sigmf_file.author` returns `sigmf_file._metadata["global"]["core:author"]
234+
235+ Parameters
236+ ----------
237+ name : str
238+ Attribute name corresponding to a core field (without "core:" prefix).
239+
240+ Returns
241+ -------
242+ value
243+ The value of the core field from global metadata, or None if not set.
244+
245+ Raises
246+ ------
247+ SigMFAccessError
248+ If the attribute name doesn't correspond to a valid core global field.
249+ """
250+ # iterate through valid global keys to find matching core field
251+ for key in self .VALID_GLOBAL_KEYS :
252+ if key .startswith ("core:" ) and key [5 :] == name :
253+ field_value = self .get_global_field (key )
254+ if field_value is None :
255+ raise SigMFAccessError (f"Core field '{ key } ' does not exist in global metadata" )
256+ return field_value
257+
258+ # if we get here, the attribute doesn't correspond to a core field
259+ raise AttributeError (f"'{ type (self ).__name__ } ' object has no attribute '{ name } '" )
260+
261+ def __setattr__ (self , name , value ):
262+ """
263+ Enable dynamic attribute setting for core global metadata fields.
264+
265+ Allows convenient setting of core metadata fields using attribute notation:
266+ - `sigmf_file.sample_rate = 1000000` sets `sigmf_file._metadata["global"]["core:sample_rate"]
267+ - `sigmf_file.author = "[email protected] "` sets `sigmf_file._metadata["global"]["core:author"] 268+
269+ Parameters
270+ ----------
271+ name : str
272+ Attribute name. If it corresponds to a core field (without "core:" prefix),
273+ the value will be set in global metadata. Otherwise, normal attribute setting occurs.
274+ value
275+ The value to set for the field.
276+ """
277+ # handle regular instance attributes, existing properties, or during initialization
278+ if (
279+ name .startswith ("_" )
280+ or hasattr (type (self ), name )
281+ or not hasattr (self , "_metadata" )
282+ or self ._metadata is None
283+ ):
284+ super ().__setattr__ (name , value )
285+ return
286+
287+ # check if this corresponds to a core global field
288+ for key in self .VALID_GLOBAL_KEYS :
289+ if key .startswith ("core:" ) and key [5 :] == name :
290+ self .set_global_field (key , value )
291+ return
292+
293+ # fall back to normal attribute setting for non-core attributes
294+ super ().__setattr__ (name , value )
295+
203296 def __next__ (self ):
204297 """get next batch of samples"""
205298 if self .iter_position < len (self ):
@@ -768,7 +861,9 @@ class SigMFCollection(SigMFMetafile):
768861 ]
769862 VALID_KEYS = {COLLECTION_KEY : VALID_COLLECTION_KEYS }
770863
771- def __init__ (self , metafiles : list = None , metadata : dict = None , base_path = None , skip_checksums : bool = False ) -> None :
864+ def __init__ (
865+ self , metafiles : list = None , metadata : dict = None , base_path = None , skip_checksums : bool = False
866+ ) -> None :
772867 """
773868 Create a SigMF Collection object.
774869
@@ -1046,6 +1141,7 @@ def fromarchive(archive_path, dir=None, skip_checksum=False):
10461141 access SigMF archives without extracting them.
10471142 """
10481143 from .archivereader import SigMFArchiveReader
1144+
10491145 return SigMFArchiveReader (archive_path , skip_checksum = skip_checksum ).sigmffile
10501146
10511147
@@ -1119,8 +1215,10 @@ def get_sigmf_filenames(filename):
11191215 # suffix, because the filename might contain '.' characters which are part
11201216 # of the filename rather than an extension.
11211217 sigmf_suffixes = [
1122- SIGMF_DATASET_EXT , SIGMF_METADATA_EXT ,
1123- SIGMF_ARCHIVE_EXT , SIGMF_COLLECTION_EXT ,
1218+ SIGMF_DATASET_EXT ,
1219+ SIGMF_METADATA_EXT ,
1220+ SIGMF_ARCHIVE_EXT ,
1221+ SIGMF_COLLECTION_EXT ,
11241222 ]
11251223 if stem_path .suffix in sigmf_suffixes :
11261224 with_suffix_path = stem_path
0 commit comments