@@ -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
@@ -202,6 +226,75 @@ def __eq__(self, other):
202226 return self ._metadata == other ._metadata
203227 return False
204228
229+ def __getattr__ (self , name ):
230+ """
231+ Enable dynamic attribute access for core global metadata fields.
232+
233+ Allows convenient access to core metadata fields using attribute notation:
234+ - `sigmf_file.sample_rate` returns `sigmf_file._metadata["global"]["core:sample_rate"]
235+ - `sigmf_file.author` returns `sigmf_file._metadata["global"]["core:author"]
236+
237+ Parameters
238+ ----------
239+ name : str
240+ Attribute name corresponding to a core field (without "core:" prefix).
241+
242+ Returns
243+ -------
244+ value
245+ The value of the core field from global metadata, or None if not set.
246+
247+ Raises
248+ ------
249+ SigMFAccessError
250+ If the attribute name doesn't correspond to a valid core global field.
251+ """
252+ # iterate through valid global keys to find matching core field
253+ for key in self .VALID_GLOBAL_KEYS :
254+ if key .startswith ("core:" ) and key [5 :] == name :
255+ field_value = self .get_global_field (key )
256+ if field_value is None :
257+ raise SigMFAccessError (f"Core field '{ key } ' does not exist in global metadata" )
258+ return field_value
259+
260+ # if we get here, the attribute doesn't correspond to a core field
261+ raise AttributeError (f"'{ type (self ).__name__ } ' object has no attribute '{ name } '" )
262+
263+ def __setattr__ (self , name , value ):
264+ """
265+ Enable dynamic attribute setting for core global metadata fields.
266+
267+ Allows convenient setting of core metadata fields using attribute notation:
268+ - `sigmf_file.sample_rate = 1000000` sets `sigmf_file._metadata["global"]["core:sample_rate"]
269+ - `sigmf_file.author = "jane.doe@domain.org"` sets `sigmf_file._metadata["global"]["core:author"]
270+
271+ Parameters
272+ ----------
273+ name : str
274+ Attribute name. If it corresponds to a core field (without "core:" prefix),
275+ the value will be set in global metadata. Otherwise, normal attribute setting occurs.
276+ value
277+ The value to set for the field.
278+ """
279+ # handle regular instance attributes, existing properties, or during initialization
280+ if (
281+ name .startswith ("_" )
282+ or hasattr (type (self ), name )
283+ or not hasattr (self , "_metadata" )
284+ or self ._metadata is None
285+ ):
286+ super ().__setattr__ (name , value )
287+ return
288+
289+ # check if this corresponds to a core global field
290+ for key in self .VALID_GLOBAL_KEYS :
291+ if key .startswith ("core:" ) and key [5 :] == name :
292+ self .set_global_field (key , value )
293+ return
294+
295+ # fall back to normal attribute setting for non-core attributes
296+ super ().__setattr__ (name , value )
297+
205298 def __next__ (self ):
206299 """get next batch of samples"""
207300 if self .iter_position < len (self ):
@@ -790,7 +883,9 @@ class SigMFCollection(SigMFMetafile):
790883 ]
791884 VALID_KEYS = {COLLECTION_KEY : VALID_COLLECTION_KEYS }
792885
793- def __init__ (self , metafiles : list = None , metadata : dict = None , base_path = None , skip_checksums : bool = False ) -> None :
886+ def __init__ (
887+ self , metafiles : list = None , metadata : dict = None , base_path = None , skip_checksums : bool = False
888+ ) -> None :
794889 """
795890 Create a SigMF Collection object.
796891
@@ -1068,6 +1163,7 @@ def fromarchive(archive_path, dir=None, skip_checksum=False):
10681163 access SigMF archives without extracting them.
10691164 """
10701165 from .archivereader import SigMFArchiveReader
1166+
10711167 return SigMFArchiveReader (archive_path , skip_checksum = skip_checksum ).sigmffile
10721168
10731169
@@ -1144,8 +1240,10 @@ def get_sigmf_filenames(filename):
11441240 # suffix, because the filename might contain '.' characters which are part
11451241 # of the filename rather than an extension.
11461242 sigmf_suffixes = [
1147- SIGMF_DATASET_EXT , SIGMF_METADATA_EXT ,
1148- SIGMF_ARCHIVE_EXT , SIGMF_COLLECTION_EXT ,
1243+ SIGMF_DATASET_EXT ,
1244+ SIGMF_METADATA_EXT ,
1245+ SIGMF_ARCHIVE_EXT ,
1246+ SIGMF_COLLECTION_EXT ,
11491247 ]
11501248 if stem_path .suffix in sigmf_suffixes :
11511249 with_suffix_path = stem_path
0 commit comments