NCCOOS Trac Projects: Top | Web | Platforms | Processing | Viz | Sprints | Sandbox | (Wind)

root/sodar/branches/scintec-branch/sodar/scintec/maindata.py

Revision 268 (checked in by cbc, 14 years ago)

Made MainData? objects callable in order to access profiles by timestamp.

Line 
1 """
2 Module to handle Scintec sodar .mnd files.
3
4 >>> from sodar.scintec import maindata
5 """
6
7 __author__ = 'Chris Calloway'
8 __email__ = 'cbc@chriscalloway.org'
9 __copyright__ = 'Copyright 2009 UNC-CH Department of Marine Science'
10 __license__ = 'GPL2'
11
12 from datetime import datetime, timedelta
13
14 class MainData(list):
15     """
16     Contain data from a Scintec sodar .mnd file.
17     
18     The data is contained as a list of Profile objects
19     along with some attributes which apply to all the Profile objects.
20     
21     Parse a known good .mnd file:
22     >>> main_data = MainData(good_mnd)
23     
24     Parse the profile data:
25     >>> len(main_data)
26     48
27     
28     Parse the first profile:
29     >>> len(main_data[0])
30     39
31     >>> main_data[0].start
32     datetime.datetime(2009, 11, 17, 0, 0)
33     >>> main_data[0].stop
34     datetime.datetime(2009, 11, 17, 0, 30)
35     >>> main_data[0].variables
36     ['z', 'speed', 'dir', 'W', 'sigW', 'bck', 'error']
37     >>> main_data[0][0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
38     ...                     'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
39     ...                     'error':'0'}
40     True
41     >>> main_data[0][0].z
42     '10'
43     >>> main_data[0]['10'] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
44     ...                     'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
45     ...                     'error':'0'}
46     True
47     >>> main_data[0]['10'].speed
48     '99.99'
49     >>> main_data[0][-1] == {'z':'200', 'speed':'99.99', 'dir':'999.9',
50     ...                      'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37',
51     ...                      'error':'0'}
52     True
53     >>> main_data[0][-1].dir
54     '999.9'
55     >>> main_data[0]['200'] == {'z':'200', 'speed':'99.99', 'dir':'999.9',
56     ...                         'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37',
57     ...                         'error':'0'}
58     True
59     >>> main_data[0]['200'].W
60     '-0.07'
61     
62     Parse the last profile:
63     >>> len(main_data[-1])
64     39
65     >>> main_data[-1].start
66     datetime.datetime(2009, 11, 17, 23, 30)
67     >>> main_data[-1].stop
68     datetime.datetime(2009, 11, 18, 0, 0)
69     >>> main_data[-1].variables
70     ['z', 'speed', 'dir', 'W', 'sigW', 'bck', 'error']
71     >>> main_data[-1][0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
72     ...                     'W':'-0.32', 'sigW':'99.99', 'bck':'9.99E+37',
73     ...                     'error':'0'}
74     True
75     >>> main_data[-1][0].z
76     '10'
77     >>> main_data[-1]['10'] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
78     ...                         'W':'-0.32', 'sigW':'99.99', 'bck':'9.99E+37',
79     ...                         'error':'0'}
80     True
81     >>> main_data[-1]['10'].speed
82     '99.99'
83     >>> main_data[-1][-1] == {'z':'200', 'speed':'15.05', 'dir':'71.8',
84     ...                      'W':'-0.19', 'sigW':'0.53', 'bck':'9.99E+37',
85     ...                      'error':'0'}
86     True
87     >>> main_data[-1][-1].dir
88     '71.8'
89     >>> main_data[-1]['200'] == {'z':'200', 'speed':'15.05', 'dir':'71.8',
90     ...                          'W':'-0.19', 'sigW':'0.53', 'bck':'9.99E+37',
91     ...                          'error':'0'}
92     True
93     >>> main_data[-1]['200'].W
94     '-0.19'
95     """
96    
97     def __init__(self, mnd, *args):
98         """
99         Parse main daily Scintec sodar .mnd file.
100         
101         MainData(mnd[,file_name[,file_path]]) -> <MainData object>
102         
103         Where:
104         
105             mnd is a str object containing the complete contents read from a
106             Scintec .mnd daily sodar file including all line endings,
107             
108             file_name is an optional str object representing a file name for
109             a file which contains the referenced .mnd daily sodar file,
110             
111             file_path is an optional str object representing the path to
112             file_name.
113         
114         Parse a known good .mnd file:
115         
116         >>> main_data = MainData(good_mnd)
117         >>> main_data = MainData(good_mnd,good_name)
118         >>> main_data.file_name == good_name
119         True
120         >>> main_data = MainData(good_mnd,good_name,good_path)
121         >>> main_data.file_name == good_name
122         True
123         >>> main_data.file_path == good_path
124         True
125         """
126        
127         super(self.__class__, self).__init__()
128        
129         self.file_name = ''
130         self.file_path = ''
131        
132         # Optional args: smoke 'em if ya got 'em.
133         try:
134             self.file_name = str(args[0])
135             self.file_path = str(args[1])
136         except IndexError:
137             pass
138        
139         # Divide the data into blocks
140         # across boundaries separated by blank lines.
141         blocks = [block.strip()
142                   for block
143                   in mnd.split('\n\n')
144                   if block.strip()]
145        
146         # The first block is the specification of the format header.
147         # Divide it into lines.
148         format_header_spec = [line.strip()
149                               for line
150                               in blocks[0].split('\n')
151                               if line.strip()]
152        
153         # The second block is the body of the format header.
154         # Divide it into lines.
155         file_header_body = [line.strip()
156                             for line
157                             in blocks[1].split('\n')
158                             if line.strip()]
159        
160         # All the remaing blocks are individual profiles
161         # in chronological order.
162         self.extend([Profile([line.strip()
163                               for line
164                               in block.split('\n')
165                               if line.strip()])
166                      for block
167                      in blocks[2:]])
168    
169     def __call__(self,timestamp,start=False):
170         """
171         Get a profile by timestamp
172         
173         maindata_instance(timestamp[,start]) -> <Profile object>
174         
175         Where:
176         
177             timestamp is a datetime.datetime object representing either
178             the start or stop timestamp of the Profile object returned.
179             
180             start is a bool object. If start is False (default), then
181             timestamp is matched to the stop timestamp of the returned
182             Profile object. Otherwise, if start is True, then timestamp is
183             matched to the start timestamp of the returned Profile object.
184         
185         Access profiles by timestamp:
186         >>> main_data = MainData(good_mnd)
187         >>> main_data(datetime(2009, 11, 17, 0, 30)).stop
188         datetime.datetime(2009, 11, 17, 0, 30)
189         >>> main_data(datetime(2009, 11, 17, 0, 0),True).start
190         datetime.datetime(2009, 11, 17, 0, 0)
191         >>> main_data(datetime(2009, 11, 18, 0, 0)).stop
192         datetime.datetime(2009, 11, 18, 0, 0)
193         >>> main_data(datetime(2009, 11, 17, 23, 0),True).start
194         datetime.datetime(2009, 11, 17, 23, 0)
195         >>> main_data(datetime(2009, 11, 16, 0, 30))
196         Traceback (most recent call last):
197             ...
198         IndexError: 'MainData' object index out of range
199         >>> main_data('OK')
200         Traceback (most recent call last):
201             ...
202         TypeError: 'MainData' object indices must be datetime.datetime
203         instances
204         """
205        
206         for profile in self:
207             if start:
208                 boundary = profile.start
209             else:
210                 boundary = profile.stop
211             if boundary == timestamp:
212                 return profile
213         if isinstance(timestamp,datetime):
214             raise IndexError, ' '.join([repr(self.__class__.__name__),
215                                         'object index out of range',])
216         else:
217             raise TypeError, ' '.join([repr(self.__class__.__name__),
218                                        'object indices must be',
219                                        'datetime.datetime instances',])
220
221 class Profile(list):
222     """
223     Contain data for single profile from a Scintec sodar .mnd file.
224     
225     The data is contained as a list of Observation objects
226     along with some attributes which apply to all the Profile objects.
227     
228     Parse a known good profile block:
229     >>> profile = Profile(good_profile)
230     
231     Access the timestamp attributes:
232     >>> profile.start
233     datetime.datetime(2009, 11, 17, 0, 0)
234     >>> profile.stop
235     datetime.datetime(2009, 11, 17, 0, 30)
236     
237     Access the variable list:
238     >>> profile.variables
239     ['z', 'speed', 'dir', 'W', 'sigW', 'bck', 'error']
240     
241     Access observations by sequence number and variables by attribute name:
242     >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
243     ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
244     ...                'error':'0'}
245     True
246     >>> profile[0].z
247     '10'
248     >>> profile[-1] == {'z':'200', 'speed':'99.99', 'dir':'999.9',
249     ...                 'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37',
250     ...                 'error':'0'}
251     True
252     >>> profile[-1].speed
253     '99.99'
254     
255     Access observations by elevation and variables by attribute name:
256     >>> profile['10'] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
257     ...                   'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
258     ...                   'error':'0'}
259     True
260     >>> profile['10'].dir
261     '999.9'
262     >>> profile['200'] == {'z':'200', 'speed':'99.99', 'dir':'999.9',
263     ...                    'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37',
264     ...                    'error':'0'}
265     True
266     >>> profile['200'].W
267     '-0.07'
268     """
269    
270     def __init__(self, profile_block):
271         """
272         Parse a profile block from a main daily Scintec sodar .mnd file.
273         
274         Profile(profile_data) -> <Profile object>
275         
276         Where:
277         
278             profile_block is a list of str objects containing all the lines
279             from a single profile in a Scintec .mnd daily sodar file.
280         
281         Parse a known good profile block:
282         >>> profile = Profile(good_profile)
283         
284         Access the timestamp attributes:
285         >>> profile.start
286         datetime.datetime(2009, 11, 17, 0, 0)
287         >>> profile.stop
288         datetime.datetime(2009, 11, 17, 0, 30)
289         
290         Access the variable list:
291         >>> profile.variables
292         ['z', 'speed', 'dir', 'W', 'sigW', 'bck', 'error']
293         
294         Access observations by sequence number and variables by attribute name:
295         >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
296         ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
297         ...                'error':'0'}
298         True
299         >>> profile[0].z
300         '10'
301         >>> profile[-1] == {'z':'200', 'speed':'99.99', 'dir':'999.9',
302         ...                 'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37',
303         ...                 'error':'0'}
304         True
305         >>> profile[-1].speed
306         '99.99'
307         """
308        
309         super(self.__class__, self).__init__()
310
311         # The first line in the profile block is the timestamp.       
312         timestamp = profile_block[0]
313         date, stop, duration = timestamp.split()
314         date_year, date_month, date_day = date.split('-')
315         stop_hour, stop_minute, stop_second = stop.split(':')
316         duration_hour, duration_minute, duration_second = duration.split(':')
317         interval = timedelta(0,                    # days
318                              int(duration_second),
319                              0,                    # microseconds
320                              0,                    # milliseconds
321                              int(duration_minute),
322                              int(duration_hour))   # omit optional weeks
323         self.stop = datetime(int(date_year),
324                              int(date_month),
325                              int(date_day),
326                              int(stop_hour),
327                              int(stop_minute),     # omit optional microseconds
328                              int(stop_second))     # and tzinfo
329         self.start = self.stop - interval
330        
331         # The second line in the profile block is
332         # a commented list of variable names
333         self.variables = profile_block[1].split()[1:]
334        
335         # All the remaining lines in the profile block are
336         # individual obervation columns in the same order
337         # as the variable names.
338         super(self.__class__, self).extend([Observation(observation.split(),
339                                                         self.variables)
340                                             for observation
341                                             in profile_block[2:]])
342    
343     def __getitem__(self,key):
344         """
345         Get a observation by elevation.
346         
347         profile_instance[elevation] -> <Observation object>
348         
349         Where:
350         
351             elevation is a str object object representing the elevation
352             (value keyed by 'z') of the Observation object.
353         
354         Access observations by elevation and variables by attribute name:
355         >>> profile = Profile(good_profile)
356         >>> profile['10'] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
357         ...                   'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
358         ...                   'error':'0'}
359         True
360         >>> profile['10'].dir
361         '999.9'
362         >>> profile['200'] == {'z':'200', 'speed':'99.99', 'dir':'999.9',
363         ...                    'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37',
364         ...                    'error':'0'}
365         True
366         >>> profile['200'].W
367         '-0.07'
368         >>> profile[100000]
369         Traceback (most recent call last):
370             ...
371         IndexError: list index out of range
372         >>> profile['100000']
373         Traceback (most recent call last):
374             ...
375         TypeError: list indices must be integers
376         """
377        
378         try:
379             return super(self.__class__, self).__getitem__(key)
380         except TypeError:
381             for observation in self:
382                 if observation['z'] == key:
383                     return observation
384             return super(self.__class__, self).__getitem__(key)
385    
386     def __setitem__(self,key,value):
387         """
388         Protect items.
389         
390         profile_instance[object1] = object2
391         
392         Raise an Exception if attempting to mutate a profile:
393         >>> profile = Profile(good_profile)
394         >>> profile[0] = 'OK'
395         Traceback (most recent call last):
396             ...
397         TypeError: 'Profile' instance's items are read-only
398         >>> profile['10'] = 'OK'
399         Traceback (most recent call last):
400             ...
401         TypeError: 'Profile' instance's items are read-only
402         >>> profile[100000] = 'OK'
403         Traceback (most recent call last):
404             ...
405         TypeError: 'Profile' instance's items are read-only
406         >>> profile[100000]
407         Traceback (most recent call last):
408             ...
409         IndexError: list index out of range
410         >>> profile['100000'] = 'OK'
411         Traceback (most recent call last):
412             ...
413         TypeError: 'Profile' instance's items are read-only
414         >>> profile['100000']
415         Traceback (most recent call last):
416             ...
417         TypeError: list indices must be integers
418         
419         Without affecting the observation:
420         >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
421         ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
422         ...                'error':'0'}
423         True
424         >>> profile['10'] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
425         ...                   'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
426         ...                   'error':'0'}
427         True
428         """
429        
430         raise TypeError, ' '.join([repr(self.__class__.__name__),
431                                    'instance\'s items are read-only',])
432    
433     def __delitem__(self,key):
434         """
435         Protect items.
436         
437         del profile_instance[object]
438         
439         Raise an Exception if attempting to mutate a profile:
440         >>> profile = Profile(good_profile)
441         >>> del profile[0]
442         Traceback (most recent call last):
443             ...
444         TypeError: 'Profile' instance's items are read-only
445         >>> del profile['10']
446         Traceback (most recent call last):
447             ...
448         TypeError: 'Profile' instance's items are read-only
449         >>> del profile[100000]
450         Traceback (most recent call last):
451             ...
452         TypeError: 'Profile' instance's items are read-only
453         >>> del profile['100000']
454         Traceback (most recent call last):
455             ...
456         TypeError: 'Profile' instance's items are read-only
457         
458         Without affecting the observation:
459         >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
460         ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
461         ...                'error':'0'}
462         True
463         >>> profile['10'] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
464         ...                   'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
465         ...                   'error':'0'}
466         True
467         """
468        
469         raise TypeError, ' '.join([repr(self.__class__.__name__),
470                                    'instance\'s items are read-only',])
471    
472     def __setslice__(self,i,j,seq):
473         """
474         Protect items.
475         
476         profile_instance[i:j] = seq
477         
478         Raise an Exception if attempting to mutate a profile:
479         >>> profile = Profile(good_profile)
480         >>> profile[0:10] = 'OK'
481         Traceback (most recent call last):
482             ...
483         TypeError: 'Profile' instance's items are read-only
484         
485         Without affecting the observations:
486         >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
487         ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
488         ...                'error':'0'}
489         True
490         >>> profile['10'] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
491         ...                   'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
492         ...                   'error':'0'}
493         True
494         """
495        
496         raise TypeError, ' '.join([repr(self.__class__.__name__),
497                                    'instance\'s items are read-only',])
498    
499     def __delslice__(self,i,j):
500         """
501         Protect items.
502         
503         del profile_instance[i:j]
504         
505         Raise an Exception if attempting to mutate a profile:
506         >>> profile = Profile(good_profile)
507         >>> del profile[0:10]
508         Traceback (most recent call last):
509             ...
510         TypeError: 'Profile' instance's items are read-only
511         
512         Without affecting the observations:
513         >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
514         ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
515         ...                'error':'0'}
516         True
517         >>> profile['10'] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
518         ...                   'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
519         ...                   'error':'0'}
520         True
521         """
522        
523         raise TypeError, ' '.join([repr(self.__class__.__name__),
524                                    'instance\'s items are read-only',])
525    
526     def append(self,item):
527         """
528         Protect items.
529         
530         Raise an Exception if attempting to mutate a profile:
531         >>> profile = Profile(good_profile)
532         >>> profile.append('OK')
533         Traceback (most recent call last):
534             ...
535         TypeError: 'Profile' instance's items are read-only
536         
537         Without affecting the observations:
538         >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
539         ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
540         ...                'error':'0'}
541         True
542         """
543        
544         raise TypeError, ' '.join([repr(self.__class__.__name__),
545                                    'instance\'s items are read-only',])
546    
547     def extend(self,seq):
548         """
549         Protect items.
550         
551         Raise an Exception if attempting to mutate a profile:
552         >>> profile = Profile(good_profile)
553         >>> profile.extend('OK')
554         Traceback (most recent call last):
555             ...
556         TypeError: 'Profile' instance's items are read-only
557         
558         Without affecting the observations:
559         >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
560         ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
561         ...                'error':'0'}
562         True
563         """
564        
565         raise TypeError, ' '.join([repr(self.__class__.__name__),
566                                    'instance\'s items are read-only',])
567    
568     def insert(self,i,item):
569         """
570         Protect items.
571         
572         Raise an Exception if attempting to mutate a profile:
573         >>> profile = Profile(good_profile)
574         >>> profile.insert(0,'OK')
575         Traceback (most recent call last):
576             ...
577         TypeError: 'Profile' instance's items are read-only
578         
579         Without affecting the observations:
580         >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
581         ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
582         ...                'error':'0'}
583         True
584         """
585        
586         raise TypeError, ' '.join([repr(self.__class__.__name__),
587                                    'instance\'s items are read-only',])
588    
589     def pop(self,*args):
590         """
591         Protect items.
592         
593         Raise an Exception if attempting to mutate a profile:
594         >>> profile = Profile(good_profile)
595         >>> profile.pop()
596         Traceback (most recent call last):
597             ...
598         TypeError: 'Profile' instance's items are read-only
599         >>> profile.pop(0)
600         Traceback (most recent call last):
601             ...
602         TypeError: 'Profile' instance's items are read-only
603         
604         Without affecting the observations:
605         >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
606         ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
607         ...                'error':'0'}
608         True
609         >>> profile[-1] == {'z':'200', 'speed':'99.99', 'dir':'999.9',
610         ...                 'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37',
611         ...                 'error':'0'}
612         True
613         """
614        
615         raise TypeError, ' '.join([repr(self.__class__.__name__),
616                                    'instance\'s items are read-only',])
617    
618     def remove(self,i):
619         """
620         Protect items.
621         
622         Raise an Exception if attempting to mutate a profile:
623         >>> profile = Profile(good_profile)
624         >>> observation = profile[0]
625         >>> profile.remove(observation)
626         Traceback (most recent call last):
627             ...
628         TypeError: 'Profile' instance's items are read-only
629         
630         Without affecting the observations:
631         >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
632         ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
633         ...                'error':'0'}
634         True
635         """
636        
637         raise TypeError, ' '.join([repr(self.__class__.__name__),
638                                    'instance\'s items are read-only',])
639    
640     def reverse(self):
641         """
642         Protect items.
643         
644         Raise an Exception if attempting to mutate a profile:
645         >>> profile = Profile(good_profile)
646         >>> profile.reverse()
647         Traceback (most recent call last):
648             ...
649         TypeError: 'Profile' instance's items are read-only
650         
651         Without affecting the observations:
652         >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
653         ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
654         ...                'error':'0'}
655         True
656         """
657        
658         raise TypeError, ' '.join([repr(self.__class__.__name__),
659                                    'instance\'s items are read-only',])
660    
661     def sort(self,**kwargs):
662         """
663         Protect items.
664         
665         Raise an Exception if attempting to mutate a profile:
666         >>> profile = Profile(good_profile)
667         >>> profile.sort()
668         Traceback (most recent call last):
669             ...
670         TypeError: 'Profile' instance's items are read-only
671         >>> profile.sort(cmp=lambda x,y: cmp(x.lower(), y.lower()))
672         Traceback (most recent call last):
673             ...
674         TypeError: 'Profile' instance's items are read-only
675         >>> profile.sort(cmp=lambda x,y: cmp(x.lower(), y.lower()),
676         ...              key=str.lower)
677         Traceback (most recent call last):
678             ...
679         TypeError: 'Profile' instance's items are read-only
680         >>> profile.sort(cmp=lambda x,y: cmp(x.lower(), y.lower()),
681         ...              key=str.lower,
682         ...              reverse=True)
683         Traceback (most recent call last):
684             ...
685         TypeError: 'Profile' instance's items are read-only
686         
687         Without affecting the observations:
688         >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
689         ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
690         ...                'error':'0'}
691         True
692         """
693        
694         raise TypeError, ' '.join([repr(self.__class__.__name__),
695                                    'instance\'s items are read-only',])
696
697 class Observation(dict):
698     """
699     Contain data for single observation from a Scintec sodar .mnd files.
700     
701     The data is contained as a dictionary of variable name/value pairs.
702     
703     The variable values may also be accessed as attributes of the instance.
704     
705     Parse a known good observation:
706     >>> observation = Observation(good_observation,good_variables)
707     >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
708     ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
709     ...                 'error':'0'}
710     True
711     
712     Access observation variables as attributes:
713     >>> observation.z
714     '10'
715     >>> observation.speed
716     '99.99'
717     >>> observation.dir
718     '999.9'
719     >>> observation.W
720     '-0.05'
721     >>> observation.sigW
722     '0.40'
723     >>> observation.bck
724     '5.46E+03'
725     >>> observation.error
726     '0'
727     """
728    
729     def __init__(self,data,variables):
730         """
731         Parse an observation from a main daily Scintec sodar .mnd file.
732         
733         Observation(data,variables) -> <Observation object>
734         
735         Where:
736         
737             data is a list of str object representing variable values,
738             
739             variables is a list of str objects representing variable labels.
740         
741         Parse a known good observation:
742         >>> observation = Observation(good_observation,good_variables)
743         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
744         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
745         ...                 'error':'0'}
746         True
747         
748         Raise an Exception if more variable names than values:
749         >>> extra_variables = good_variables + ['extra']
750         >>> observation = Observation(good_observation,extra_variables)
751         Traceback (most recent call last):
752             ...
753         AttributeError: Same number of attributes and values required in
754         'Observation' object
755         
756         Raise an Exception if more variable values than names:
757         >>> extra_observation = good_observation + ['extra']
758         >>> observation = Observation(extra_observation,good_variables)
759         Traceback (most recent call last):
760             ...
761         AttributeError: Same number of attributes and values required in
762         'Observation' object
763         """
764        
765         super(self.__class__, self).__init__()
766        
767         if len(variables) == len(data):
768             # And update the dictionary with the variable names/values.
769             super(self.__class__, self).update(dict(zip(variables,data)))
770         else:
771             # Unless there are an unequal number of variable names/values.
772             raise AttributeError, ' '.join(['Same number of attributes',
773                                             'and values required in',
774                                             repr(self.__class__.__name__),
775                                             'object',])
776    
777     def __getattr__(self,name):
778         """
779         Get a variable value by name.
780         
781         observation_instance.variable_name -> variable_value
782         
783         Access observation variables as attributes:
784         >>> observation = Observation(good_observation,good_variables)
785         >>> observation.z
786         '10'
787         >>> observation.speed
788         '99.99'
789         >>> observation.dir
790         '999.9'
791         >>> observation.W
792         '-0.05'
793         >>> observation.sigW
794         '0.40'
795         >>> observation.bck
796         '5.46E+03'
797         >>> observation.error
798         '0'
799         
800         Raise an Exception for an undefined attribute:
801         >>> observation.y
802         Traceback (most recent call last):
803             ...
804         AttributeError: 'Observation' object has no attribute 'y'
805         
806         Allow the creation and access of attributes other than variables.
807         >>> observation.y = 'OK'
808         >>> observation.y
809         'OK'
810         
811         Without affecting the variables:
812         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
813         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
814         ...                 'error':'0'}
815         True
816         """
817        
818         # Attempt attribute access by dictionary key.
819         if name in self:
820             return self[name]
821         else:
822             raise AttributeError, ' '.join([repr(self.__class__.__name__),
823                                             'object has no attribute',
824                                             repr(name),])
825    
826     def __setattr__(self,name,value):
827         """
828         Protect variable values from overwriting.
829         
830         observation_instance.variable_name = variable_value
831         
832         But retrieve any other bound attribute values.
833         
834         Raise an Exception if attempting to rebind a variable:
835         >>> observation = Observation(good_observation,good_variables)
836         >>> observation.z = 'OK'
837         Traceback (most recent call last):
838             ...
839         AttributeError: Rebinding attribute 'z' disallowed in
840         'Observation' object
841         
842         However, allow attributes other than variables to be bound:
843         >>> observation.y = 'OK'
844         >>> observation.y
845         'OK'
846         
847         Without affecting the variables:
848         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
849         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
850         ...                 'error':'0'}
851         True
852         """
853        
854         if name in self:
855             # Protect observation variables
856             raise AttributeError, ' '.join(['Rebinding attribute',
857                                             repr(name),
858                                             'disallowed in',
859                                             repr(self.__class__.__name__),
860                                             'object',])
861         else:
862             # Otherwise, do it the normal way.
863             super(self.__class__, self).__setattr__(name,value)
864    
865     def __delattr__(self,name):
866         """
867         Protect variable values from deletion.
868         
869         del observation_instance.variable_name
870         
871         But delete any other bound attribute values.
872         
873         Raise an Exception if attempting to unbind a variable:
874         >>> observation = Observation(good_observation,good_variables)
875         >>> del observation.z
876         Traceback (most recent call last):
877             ...
878         AttributeError: Unbinding attribute 'z' disallowed in
879         'Observation' object
880         
881         Raise an Exception for attempting to unbind an undefined attribute:
882         >>> del observation.y
883         Traceback (most recent call last):
884             ...
885         AttributeError: 'Observation' object has no attribute 'y'
886         
887         However, allow attributes other than variables to be unbound:
888         >>> observation.y = 'OK'
889         >>> del observation.y
890         >>> del observation.y
891         Traceback (most recent call last):
892             ...
893         AttributeError: y
894         
895         Without affecting the variables:
896         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
897         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
898         ...                 'error':'0'}
899         True
900         """
901        
902         if name in self:
903             # Protect observation variables
904             raise AttributeError, ' '.join(['Unbinding attribute',
905                                             repr(name),
906                                             'disallowed in',
907                                             repr(self.__class__.__name__),
908                                             'object',])
909         else:
910             # Otherwise, do it the normal way.
911             super(self.__class__, self).__delattr__(name)
912    
913     def __setitem__(self,key,value):
914         """
915         Protect items.
916         
917         observation_instance[variable_name] = variable_value
918         
919         Raise an Exception if attempting to mutate an observation:
920         >>> observation = Observation(good_observation,good_variables)
921         >>> observation['z'] = 'OK'
922         Traceback (most recent call last):
923             ...
924         TypeError: 'Observation' instance's items are read-only
925         >>> observation['y'] = 'OK'
926         Traceback (most recent call last):
927             ...
928         TypeError: 'Observation' instance's items are read-only
929         >>> observation.y
930         Traceback (most recent call last):
931             ...
932         AttributeError: 'Observation' object has no attribute 'y'
933         >>> observation.y = 'OK'
934         >>> observation['y'] = 'Not OK'
935         Traceback (most recent call last):
936             ...
937         TypeError: 'Observation' instance's items are read-only
938         >>> observation.y
939         'OK'
940         
941         Without affecting the variables:
942         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
943         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
944         ...                 'error':'0'}
945         True
946         """
947        
948         raise TypeError, ' '.join([repr(self.__class__.__name__),
949                                    'instance\'s items are read-only',])
950    
951     def __delitem__(self,key):
952         """
953         Protect items.
954         
955         observation_instance[variable_name] = variable_value
956         
957         Raise an Exception if attempting to mutate an observation:
958         >>> observation = Observation(good_observation,good_variables)
959         >>> del observation['z']
960         Traceback (most recent call last):
961             ...
962         TypeError: 'Observation' instance's items are read-only
963         >>> observation.y = 'OK'
964         >>> del observation['y']
965         Traceback (most recent call last):
966             ...
967         TypeError: 'Observation' instance's items are read-only
968         
969         Without affecting the variables:
970         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
971         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
972         ...                 'error':'0'}
973         True
974         """
975        
976         raise TypeError, ' '.join([repr(self.__class__.__name__),
977                                    'instance\'s items are read-only',])
978    
979     def clear(self):
980         """
981         Protect items.
982         
983         Raise an Exception if attempting to mutate an observation:
984         >>> observation = Observation(good_observation,good_variables)
985         >>> observation.clear()
986         Traceback (most recent call last):
987             ...
988         TypeError: 'Observation' instance's items are read-only
989         
990         Without affecting the variables:
991         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
992         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
993         ...                 'error':'0'}
994         True
995         """
996        
997         raise TypeError, ' '.join([repr(self.__class__.__name__),
998                                    'instance\'s items are read-only',])
999    
1000     def pop(self,key,*args):
1001         """
1002         Protect items.
1003         
1004         Raise an Exception if attempting to mutate an observation:
1005         >>> observation = Observation(good_observation,good_variables)
1006         >>> observation.pop('z')
1007         Traceback (most recent call last):
1008             ...
1009         TypeError: 'Observation' instance's items are read-only
1010         >>> observation.pop('y')
1011         Traceback (most recent call last):
1012             ...
1013         TypeError: 'Observation' instance's items are read-only
1014         >>> observation.pop('y','OK')
1015         Traceback (most recent call last):
1016             ...
1017         TypeError: 'Observation' instance's items are read-only
1018         
1019         Without affecting the variables:
1020         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
1021         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
1022         ...                 'error':'0'}
1023         True
1024         """
1025        
1026         raise TypeError, ' '.join([repr(self.__class__.__name__),
1027                                    'instance\'s items are read-only',])
1028    
1029     def popitem(self):
1030         """
1031         Protect items.
1032         
1033         Raise an Exception if attempting to mutate an observation:
1034         >>> observation = Observation(good_observation,good_variables)
1035         >>> observation.popitem()
1036         Traceback (most recent call last):
1037             ...
1038         TypeError: 'Observation' instance's items are read-only
1039         
1040         Without affecting the variables:
1041         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
1042         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
1043         ...                 'error':'0'}
1044         True
1045         """
1046        
1047         raise TypeError, ' '.join([repr(self.__class__.__name__),
1048                                    'instance\'s items are read-only',])
1049    
1050     def setdefault(self,key,*args):
1051         """
1052         Protect items.
1053         
1054         Raise an Exception if attempting to mutate an observation:
1055         >>> observation = Observation(good_observation,good_variables)
1056         >>> observation.setdefault('z')
1057         Traceback (most recent call last):
1058             ...
1059         TypeError: 'Observation' instance's items are read-only
1060         >>> observation.setdefault('y')
1061         Traceback (most recent call last):
1062             ...
1063         TypeError: 'Observation' instance's items are read-only
1064         >>> observation.setdefault('y','OK')
1065         Traceback (most recent call last):
1066             ...
1067         TypeError: 'Observation' instance's items are read-only
1068         
1069         Without affecting the variables:
1070         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
1071         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
1072         ...                 'error':'0'}
1073         True
1074         """
1075        
1076         raise TypeError, ' '.join([repr(self.__class__.__name__),
1077                                    'instance\'s items are read-only',])
1078    
1079     def update(self,*args,**kwargs):
1080         """
1081         Protect items.
1082         
1083         Raise an Exception if attempting to mutate an observation:
1084         >>> observation = Observation(good_observation,good_variables)
1085         >>> observation.update({'UP':'up','DOWN':'down',})
1086         Traceback (most recent call last):
1087             ...
1088         TypeError: 'Observation' instance's items are read-only
1089         
1090         Without affecting the variables:
1091         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
1092         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
1093         ...                 'error':'0'}
1094         True
1095         
1096         Raise an Exception if attempting to mutate an observation:
1097         >>> observation.update((('UP','up'),('DOWN','down'),))
1098         Traceback (most recent call last):
1099             ...
1100         TypeError: 'Observation' instance's items are read-only
1101
1102         Without affecting the variables:
1103         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
1104         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
1105         ...                 'error':'0'}
1106         True
1107         
1108         Raise an Exception if attempting to mutate an observation:
1109         >>> observation.update(UP='up',DOWN='down')
1110         Traceback (most recent call last):
1111             ...
1112         TypeError: 'Observation' instance's items are read-only
1113         
1114         Without affecting the variables:
1115         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
1116         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
1117         ...                 'error':'0'}
1118         True
1119         """
1120        
1121         raise TypeError, ' '.join([repr(self.__class__.__name__),
1122                                    'instance\'s items are read-only',])
1123
1124 def _test():
1125     """
1126     Run module tests in script mode.
1127     
1128     >>> from sodar.scintec.maindata import _test
1129     """
1130    
1131     import doctest
1132     from sodar.tests import suite
1133     mnd_path,mnd_file,mnd,profile,variables,observation = \
1134         suite.setUpGoodMndData()
1135     doctest.testmod(extraglobs=dict(good_mnd=mnd,
1136                                     good_name=mnd_file,
1137                                     good_path=mnd_path,
1138                                     good_profile=profile,
1139                                     good_variables=variables,
1140                                     good_observation=observation),
1141                     optionflags=doctest.NORMALIZE_WHITESPACE)
1142
1143 if __name__ == "__main__":
1144     _test()
Note: See TracBrowser for help on using the browser.