openrvdas.logger.utils.base_module

The biggest thing that this abstract parent class does is help with (optional) type checking of the child class' inputs and outputs. In the past an explicit, but very awkward, form of type checking was used, with child classes passing the Transform class a list of input_format and output_format specifications.

That is now deprecated in favor of using Python's type hints. Type hints should be specified for the child class' read(), transform() or write() method. Then the method can call self.can_process_record(record) to see whether it's one of the input types it can handle, and/or check_result to see if the output is as expected. If not, it can "return self.digest_record(record) to have the parent class try to deal with it:

E.g.: def transform(self, record: Union[int, str, float]): if not self.can_process_record(record): # inherited from BaseModule() return self.digest_record(record) # inherited from BaseModule() return str(record) + '+'

If no type hints are specified, can_process_record() will return True for all records except those of type "None" or "list". The logic is that digest_record() will return a None when given a None, and when given a list, will iteratively apply the transform to every element of the list and return the resulting list.

Note that the child class can explicitly call super().__init__(quiet=True) or such to initialize the type checking and set its debugging level. If it is not explicitly initialized, it will be done implicitly the first time can_process_record() or digest_record() are called, but with the default of quiet=False.

  1#!/usr/bin/env python3
  2"""
  3The biggest thing that this abstract parent class does is help with (optional)
  4type checking of the child class' inputs and outputs. In the past an explicit,
  5but very awkward, form of type checking was used, with child classes passing the
  6Transform class a list of input_format and output_format specifications.
  7
  8That is now deprecated in favor of using Python's type hints. Type hints should be
  9specified for the child class' read(), transform() or write() method. Then the method can
 10call self.can_process_record(record) to see whether it's one of the input types it
 11can handle, and/or check_result to see if the output is as expected. If not, it can
 12"return self.digest_record(record) to have the parent class try to deal with it:
 13
 14E.g.:
 15     def transform(self, record: Union[int, str, float]):
 16        if not self.can_process_record(record):  # inherited from BaseModule()
 17            return self.digest_record(record)  # inherited from BaseModule()
 18         return str(record) + '+'
 19
 20If no type hints are specified, can_process_record() will return True for all
 21records *except* those of type "None" or "list". The logic is that digest_record()
 22will return a None when given a None, and when given a list, will iteratively
 23apply the transform to every element of the list and return the resulting list.
 24
 25Note that the child class can explicitly call super().__init__(quiet=True) or such
 26to initialize the type checking and set its debugging level. If it is not explicitly
 27initialized, it will be done implicitly the first time can_process_record() or
 28digest_record() are called, but with the default of quiet=False.
 29"""
 30import inspect
 31import logging
 32import threading
 33import queue
 34from typing import get_args
 35
 36from logger.utils.das_record import DASRecord  # noqa E402
 37
 38
 39########################################
 40def get_method_type_hints(method):
 41    """
 42    When passed a method via something like
 43
 44        get_method_type_hints(self.__class__.transform)
 45
 46    return a dict of the method's type hints for the arguments
 47    and return value. E.g., if transform() is defined as:
 48
 49        def transform(self, record: int|float) -> str:
 50
 51    will return
 52
 53        {'return': (<class 'str'>),
 54         'record': (<class 'int'>, <class 'float'>)}
 55
 56    The point of this routine is to allow Transform and Writer to sanity
 57    check their inputs.
 58    """
 59    method_args = inspect.getfullargspec(method).annotations
 60    method_types = {k: tuple([value])
 61                    if isinstance(value, type)
 62                    else tuple(get_args(value))
 63                    for k, value in method_args.items()
 64                    }
 65    return method_types
 66
 67
 68################################################################################
 69class BaseModule:
 70    """
 71    Base class for OpenRVDAS Readers, Transforms and Writers.
 72
 73    Implements method for checking whether a received record is in a format
 74    that the derived class can process, and also a method for splitting a
 75    list of records into its elements and calling subclass transform() on them.
 76
 77    Starting with v0.6, BaseModule also implements "mirroring" functionality.
 78    If the optional 'mirror_to' argument is passed to the constructor (and
 79    the subclass is valid for mirroring, i.e. not a Writer), BaseModule
 80    will spin up a thread and a queue to asynchronously write a copy of
 81    every record it processes to the specified Writer.
 82    """
 83    ############################
 84    def __init__(self, quiet=False, encoding='utf-8', encoding_errors='ignore',
 85                 mirror_to=None, *args, **kwargs):
 86        """
 87        ```
 88        quiet - if type checking should log type errors or operate silently.
 89
 90        Two additional arguments govern how records will be encoded/decoded
 91        from bytes, if desired by the Writer subclass when it calls
 92        _encode_str() or _decode_bytes:
 93
 94        encoding - 'utf-8' by default. If empty or None, do not attempt any
 95                decoding and return raw bytes. Other possible encodings are
 96                listed in online documentation here:
 97                https://docs.python.org/3/library/codecs.html#standard-encodings
 98
 99        encoding_errors - 'ignore' by default. Other error strategies are
100                'strict', 'replace', and 'backslashreplace', described here:
101                https://docs.python.org/3/howto/unicode.html#encodings
102
103        mirror_to - Optional Writer to which all records read or transformed
104                by this module (if it is a Reader or Transform) will be
105                "mirrored" (copied). Mirroring happens asynchronously via
106                a queue and background thread to minimize impact on the
107                primary data flow. Writers cannot be mirrored.
108        ```
109        """
110        if kwargs.get('input_format'):
111            logging.warning(f'Code warning: {self.__class__.__name__} use of "input_format"'
112                            'is deprecated in favor of type hints. Please see documentation'
113                            'in logger/utils/base_module.py.')
114        if kwargs.get('output_format'):
115            logging.warning(f'Code warning: {self.__class__.__name__} use of "output_format"'
116                            'is deprecated in favor of type hints. Please see documentation'
117                            'in logger/utils/base_module.py.')
118        self.quiet = quiet
119
120        # Make sure '' behaves the same as None, which is what all the
121        # docstrings say, and would be logical... but then certain things treat
122        # them differently (e.g., file.open(mode='ab', encoding='') throws
123        # ValueError: binary mode doesn't take an encoding argument)
124        if encoding == '':
125            encoding = None
126        self.encoding = encoding
127        self.encoding_errors = encoding_errors
128
129        # Handle mirroring
130        self.mirror_to = mirror_to
131        if self.mirror_to:
132            from logger.writers.writer import Writer
133            if isinstance(self, Writer):
134                logging.warning(f'Writer {self.__class__.__name__} passed "mirror_to" argument. '
135                                'Writers cannot be mirrored.')
136                self.mirror_to = None
137            elif not isinstance(self.mirror_to, Writer):
138                raise TypeError(f'mirror_to must be a Writer, not {type(self.mirror_to)}')
139            else:
140                self.mirror_queue = queue.Queue()
141                self.mirror_thread = threading.Thread(target=self._mirror_output_thread,
142                                                      daemon=True)
143                self.mirror_thread.start()
144
145                # Dynamically wrap the read() or transform() method
146                if hasattr(self, 'read'):
147                    self._original_read = self.read
148                    self.read = self._wrapped_read
149                elif hasattr(self, 'transform'):
150                    self._original_transform = self.transform
151                    self.transform = self._wrapped_transform
152
153    def _mirror_output_thread(self):
154        """Thread to pull records from the queue and write them to the
155        mirror_to writer."""
156        while True:
157            record = self.mirror_queue.get()
158            try:
159                self.mirror_to.write(record)
160            except Exception as e:
161                logging.warning(f'Error writing to mirror_to: {e}')
162
163    def _wrapped_read(self):
164        """Wrapped version of read() that intercepts records and sends
165        them to the mirror_to writer."""
166        record = self._original_read()
167        if record is not None:
168            self.mirror_queue.put(record)
169        return record
170
171    def _wrapped_transform(self, record):
172        """Wrapped version of transform() that intercepts records and sends
173        them to the mirror_to writer."""
174        result = self._original_transform(record)
175        if result is not None:
176            self.mirror_queue.put(result)
177        return result
178
179    ############################
180    def _initialize_type_hints(self, module_type, module_method):
181        """We should only get called from the _initialize_type_hints method of
182        Reader/Transform/Writer subclasses, which should fill in all parameters.
183
184        Retrieve any type hints for child read()/transform()/write() method so we
185        can check whether the type of record we've received can be parsed
186        natively or not."""
187        self.module_type = module_type
188        self.module_method = module_method
189
190        # We make stupid assumption that the input variable is called 'record'
191        method_type_hints = get_method_type_hints(self.module_method)
192        self.input_types = method_type_hints.get('record')
193        self.return_types = method_type_hints.get('return')
194
195        # logging.warning(f'input_types: {self.input_types}')
196        # logging.warning(f'return_types: {self.return_types}')
197
198        # Other things we'd want to make sure are defined.
199        self.class_name = self.__class__.__name__
200        self.initialized = True
201
202    ############################
203    def can_process_record(self, record):
204        """ Is this record in a format that the transform or writer can handle?
205
206        - If there are type hints: True if type of record is in type hints.
207        - If there are no type hints: False if None or list, otherwise True.
208
209        The logic is that if there are no type hints and we see False or
210        a list, we expect digest_record() to be called to deal with it."""
211
212        try:  # if we've not been initialized with type hints, initialize now
213            self.initialized or True
214        except AttributeError:
215            # This will call the subclass initialization, e.g.
216            # Transform._initialize_type_hints(), which will in turn call
217            # OpenRVDASModule._initialize_type_hints()
218            self._initialize_type_hints()
219
220        # Special case: we want to turn empty str records into None. By saying
221        # no, record should get punted to digest_record(), which will do the
222        # right thing.
223        if isinstance(record, str) and not len(record):
224            return False
225
226        if self.input_types:
227            return isinstance(record, self.input_types)
228
229        # If not type hints, make some judgment calls. Say "no" to None
230        # and to lists, because we'll expect that answer to trigger a
231        # call to digest_record(), which will handle them.
232        if record is None or isinstance(record, list):
233            return False
234        return True
235
236    ############################
237    def digest_record(self, record):
238        """ Try to digest record down into a format that the method can
239        handle. Typically that will mean that we've been handed a list of
240        records that we need to break into individual records."""
241
242        try:  # if we've not been initialized with type hints, initialize now
243            self.initialized or True
244        except AttributeError:
245            self._initialize_type_hints()
246
247        # Go through our litany of things that reduce to None
248        if record is None:
249            return None
250
251        if isinstance(record, str) and not len(record):
252            return None
253
254        # If it's a type the method can handle directly (though, if so,
255        # why were we called?!?
256        if self.can_process_record(record) and not self.quiet:
257            logging.warning(f'{self.class_name}: digest_record() called unnecessarily.')
258            logging.warning(f'Can process {self.input_types}; received {type(record)}: {record}')
259            return self.module_method(record)
260
261        # We know how to deal with it if it's a list: Apply to components,
262        # stripping out any None's
263        if isinstance(record, list):
264            result = [self.module_method(self, r) for r in record if r is not None]
265            return [r for r in result if r is not None]  # remove Nones
266
267        # Is record a number we can convert to a string?
268        if str in self.input_types and isinstance(record, (int, float)):
269            return str(record)
270
271        # If it's a DASRecord, serialize it as JSON
272        if str in self.input_types and isinstance(record, DASRecord):
273            return record.as_json()
274
275        # If we don't know how to deal with it
276        if not self.quiet:
277            logging.warning(f'Unable to convert record to format "{self.class_name}" can process')
278            logging.warning(f'Must be instance or list of {self.input_types}')
279            logging.warning(f'Received {type(record)}: {record}')
280        return None
281
282    ############################
283    def _unescape_str(self, the_str):
284        """Unescape a string by encoding it to bytes, then unescaping when we
285        decode it. Ugly.
286        """
287        if not self.encoding:
288            return the_str
289
290        encoded = the_str.encode(encoding=self.encoding, errors=self.encoding_errors)
291        return encoded.decode('unicode_escape')
292
293    ############################
294    def _encode_str(self, the_str, unescape=False):
295        """Encode a string to bytes, optionally unescaping things like \n and \r.
296        Unescaping requires ugly convolutions of encoding, then decoding while we
297        escape things, then encoding a second time.
298        """
299        if not self.encoding:
300            return the_str
301        if unescape:
302            the_str = self._unescape_str(the_str)
303        return the_str.encode(encoding=self.encoding, errors=self.encoding_errors)
304
305    ############################
306    def _decode_bytes(self, record, allow_empty: bool = False):
307        """Decode a record from bytes to str, if we have an encoding specified."""
308        if record is None:
309            return None
310
311        if not record and not allow_empty:  # if it's an empty record but not None
312            return None
313
314        if not self.encoding:
315            return record
316
317        if self.encoding == 'hex':
318            try:
319                r = record.hex()
320                return r
321            except Exception as e:
322                logging.warning('Error decoding string "%s" from encoding "%s": %s',
323                                record, self.encoding, str(e))
324                return None
325
326        try:
327            return record.decode(encoding=self.encoding,
328                                 errors=self.encoding_errors)
329        except UnicodeDecodeError as e:
330            logging.warning('Error decoding string "%s" from encoding "%s": %s',
331                            record, self.encoding, str(e))
332            return None
def get_method_type_hints(method):
41def get_method_type_hints(method):
42    """
43    When passed a method via something like
44
45        get_method_type_hints(self.__class__.transform)
46
47    return a dict of the method's type hints for the arguments
48    and return value. E.g., if transform() is defined as:
49
50        def transform(self, record: int|float) -> str:
51
52    will return
53
54        {'return': (<class 'str'>),
55         'record': (<class 'int'>, <class 'float'>)}
56
57    The point of this routine is to allow Transform and Writer to sanity
58    check their inputs.
59    """
60    method_args = inspect.getfullargspec(method).annotations
61    method_types = {k: tuple([value])
62                    if isinstance(value, type)
63                    else tuple(get_args(value))
64                    for k, value in method_args.items()
65                    }
66    return method_types

When passed a method via something like

get_method_type_hints(self.__class__.transform)

return a dict of the method's type hints for the arguments and return value. E.g., if transform() is defined as:

def transform(self, record: int|float) -> str:

will return

{'return': (<class 'str'>),
 'record': (<class 'int'>, <class 'float'>)}

The point of this routine is to allow Transform and Writer to sanity check their inputs.

class BaseModule:
 70class BaseModule:
 71    """
 72    Base class for OpenRVDAS Readers, Transforms and Writers.
 73
 74    Implements method for checking whether a received record is in a format
 75    that the derived class can process, and also a method for splitting a
 76    list of records into its elements and calling subclass transform() on them.
 77
 78    Starting with v0.6, BaseModule also implements "mirroring" functionality.
 79    If the optional 'mirror_to' argument is passed to the constructor (and
 80    the subclass is valid for mirroring, i.e. not a Writer), BaseModule
 81    will spin up a thread and a queue to asynchronously write a copy of
 82    every record it processes to the specified Writer.
 83    """
 84    ############################
 85    def __init__(self, quiet=False, encoding='utf-8', encoding_errors='ignore',
 86                 mirror_to=None, *args, **kwargs):
 87        """
 88        ```
 89        quiet - if type checking should log type errors or operate silently.
 90
 91        Two additional arguments govern how records will be encoded/decoded
 92        from bytes, if desired by the Writer subclass when it calls
 93        _encode_str() or _decode_bytes:
 94
 95        encoding - 'utf-8' by default. If empty or None, do not attempt any
 96                decoding and return raw bytes. Other possible encodings are
 97                listed in online documentation here:
 98                https://docs.python.org/3/library/codecs.html#standard-encodings
 99
100        encoding_errors - 'ignore' by default. Other error strategies are
101                'strict', 'replace', and 'backslashreplace', described here:
102                https://docs.python.org/3/howto/unicode.html#encodings
103
104        mirror_to - Optional Writer to which all records read or transformed
105                by this module (if it is a Reader or Transform) will be
106                "mirrored" (copied). Mirroring happens asynchronously via
107                a queue and background thread to minimize impact on the
108                primary data flow. Writers cannot be mirrored.
109        ```
110        """
111        if kwargs.get('input_format'):
112            logging.warning(f'Code warning: {self.__class__.__name__} use of "input_format"'
113                            'is deprecated in favor of type hints. Please see documentation'
114                            'in logger/utils/base_module.py.')
115        if kwargs.get('output_format'):
116            logging.warning(f'Code warning: {self.__class__.__name__} use of "output_format"'
117                            'is deprecated in favor of type hints. Please see documentation'
118                            'in logger/utils/base_module.py.')
119        self.quiet = quiet
120
121        # Make sure '' behaves the same as None, which is what all the
122        # docstrings say, and would be logical... but then certain things treat
123        # them differently (e.g., file.open(mode='ab', encoding='') throws
124        # ValueError: binary mode doesn't take an encoding argument)
125        if encoding == '':
126            encoding = None
127        self.encoding = encoding
128        self.encoding_errors = encoding_errors
129
130        # Handle mirroring
131        self.mirror_to = mirror_to
132        if self.mirror_to:
133            from logger.writers.writer import Writer
134            if isinstance(self, Writer):
135                logging.warning(f'Writer {self.__class__.__name__} passed "mirror_to" argument. '
136                                'Writers cannot be mirrored.')
137                self.mirror_to = None
138            elif not isinstance(self.mirror_to, Writer):
139                raise TypeError(f'mirror_to must be a Writer, not {type(self.mirror_to)}')
140            else:
141                self.mirror_queue = queue.Queue()
142                self.mirror_thread = threading.Thread(target=self._mirror_output_thread,
143                                                      daemon=True)
144                self.mirror_thread.start()
145
146                # Dynamically wrap the read() or transform() method
147                if hasattr(self, 'read'):
148                    self._original_read = self.read
149                    self.read = self._wrapped_read
150                elif hasattr(self, 'transform'):
151                    self._original_transform = self.transform
152                    self.transform = self._wrapped_transform
153
154    def _mirror_output_thread(self):
155        """Thread to pull records from the queue and write them to the
156        mirror_to writer."""
157        while True:
158            record = self.mirror_queue.get()
159            try:
160                self.mirror_to.write(record)
161            except Exception as e:
162                logging.warning(f'Error writing to mirror_to: {e}')
163
164    def _wrapped_read(self):
165        """Wrapped version of read() that intercepts records and sends
166        them to the mirror_to writer."""
167        record = self._original_read()
168        if record is not None:
169            self.mirror_queue.put(record)
170        return record
171
172    def _wrapped_transform(self, record):
173        """Wrapped version of transform() that intercepts records and sends
174        them to the mirror_to writer."""
175        result = self._original_transform(record)
176        if result is not None:
177            self.mirror_queue.put(result)
178        return result
179
180    ############################
181    def _initialize_type_hints(self, module_type, module_method):
182        """We should only get called from the _initialize_type_hints method of
183        Reader/Transform/Writer subclasses, which should fill in all parameters.
184
185        Retrieve any type hints for child read()/transform()/write() method so we
186        can check whether the type of record we've received can be parsed
187        natively or not."""
188        self.module_type = module_type
189        self.module_method = module_method
190
191        # We make stupid assumption that the input variable is called 'record'
192        method_type_hints = get_method_type_hints(self.module_method)
193        self.input_types = method_type_hints.get('record')
194        self.return_types = method_type_hints.get('return')
195
196        # logging.warning(f'input_types: {self.input_types}')
197        # logging.warning(f'return_types: {self.return_types}')
198
199        # Other things we'd want to make sure are defined.
200        self.class_name = self.__class__.__name__
201        self.initialized = True
202
203    ############################
204    def can_process_record(self, record):
205        """ Is this record in a format that the transform or writer can handle?
206
207        - If there are type hints: True if type of record is in type hints.
208        - If there are no type hints: False if None or list, otherwise True.
209
210        The logic is that if there are no type hints and we see False or
211        a list, we expect digest_record() to be called to deal with it."""
212
213        try:  # if we've not been initialized with type hints, initialize now
214            self.initialized or True
215        except AttributeError:
216            # This will call the subclass initialization, e.g.
217            # Transform._initialize_type_hints(), which will in turn call
218            # OpenRVDASModule._initialize_type_hints()
219            self._initialize_type_hints()
220
221        # Special case: we want to turn empty str records into None. By saying
222        # no, record should get punted to digest_record(), which will do the
223        # right thing.
224        if isinstance(record, str) and not len(record):
225            return False
226
227        if self.input_types:
228            return isinstance(record, self.input_types)
229
230        # If not type hints, make some judgment calls. Say "no" to None
231        # and to lists, because we'll expect that answer to trigger a
232        # call to digest_record(), which will handle them.
233        if record is None or isinstance(record, list):
234            return False
235        return True
236
237    ############################
238    def digest_record(self, record):
239        """ Try to digest record down into a format that the method can
240        handle. Typically that will mean that we've been handed a list of
241        records that we need to break into individual records."""
242
243        try:  # if we've not been initialized with type hints, initialize now
244            self.initialized or True
245        except AttributeError:
246            self._initialize_type_hints()
247
248        # Go through our litany of things that reduce to None
249        if record is None:
250            return None
251
252        if isinstance(record, str) and not len(record):
253            return None
254
255        # If it's a type the method can handle directly (though, if so,
256        # why were we called?!?
257        if self.can_process_record(record) and not self.quiet:
258            logging.warning(f'{self.class_name}: digest_record() called unnecessarily.')
259            logging.warning(f'Can process {self.input_types}; received {type(record)}: {record}')
260            return self.module_method(record)
261
262        # We know how to deal with it if it's a list: Apply to components,
263        # stripping out any None's
264        if isinstance(record, list):
265            result = [self.module_method(self, r) for r in record if r is not None]
266            return [r for r in result if r is not None]  # remove Nones
267
268        # Is record a number we can convert to a string?
269        if str in self.input_types and isinstance(record, (int, float)):
270            return str(record)
271
272        # If it's a DASRecord, serialize it as JSON
273        if str in self.input_types and isinstance(record, DASRecord):
274            return record.as_json()
275
276        # If we don't know how to deal with it
277        if not self.quiet:
278            logging.warning(f'Unable to convert record to format "{self.class_name}" can process')
279            logging.warning(f'Must be instance or list of {self.input_types}')
280            logging.warning(f'Received {type(record)}: {record}')
281        return None
282
283    ############################
284    def _unescape_str(self, the_str):
285        """Unescape a string by encoding it to bytes, then unescaping when we
286        decode it. Ugly.
287        """
288        if not self.encoding:
289            return the_str
290
291        encoded = the_str.encode(encoding=self.encoding, errors=self.encoding_errors)
292        return encoded.decode('unicode_escape')
293
294    ############################
295    def _encode_str(self, the_str, unescape=False):
296        """Encode a string to bytes, optionally unescaping things like \n and \r.
297        Unescaping requires ugly convolutions of encoding, then decoding while we
298        escape things, then encoding a second time.
299        """
300        if not self.encoding:
301            return the_str
302        if unescape:
303            the_str = self._unescape_str(the_str)
304        return the_str.encode(encoding=self.encoding, errors=self.encoding_errors)
305
306    ############################
307    def _decode_bytes(self, record, allow_empty: bool = False):
308        """Decode a record from bytes to str, if we have an encoding specified."""
309        if record is None:
310            return None
311
312        if not record and not allow_empty:  # if it's an empty record but not None
313            return None
314
315        if not self.encoding:
316            return record
317
318        if self.encoding == 'hex':
319            try:
320                r = record.hex()
321                return r
322            except Exception as e:
323                logging.warning('Error decoding string "%s" from encoding "%s": %s',
324                                record, self.encoding, str(e))
325                return None
326
327        try:
328            return record.decode(encoding=self.encoding,
329                                 errors=self.encoding_errors)
330        except UnicodeDecodeError as e:
331            logging.warning('Error decoding string "%s" from encoding "%s": %s',
332                            record, self.encoding, str(e))
333            return None

Base class for OpenRVDAS Readers, Transforms and Writers.

Implements method for checking whether a received record is in a format that the derived class can process, and also a method for splitting a list of records into its elements and calling subclass transform() on them.

Starting with v0.6, BaseModule also implements "mirroring" functionality. If the optional 'mirror_to' argument is passed to the constructor (and the subclass is valid for mirroring, i.e. not a Writer), BaseModule will spin up a thread and a queue to asynchronously write a copy of every record it processes to the specified Writer.

BaseModule( quiet=False, encoding='utf-8', encoding_errors='ignore', mirror_to=None, *args, **kwargs)
 85    def __init__(self, quiet=False, encoding='utf-8', encoding_errors='ignore',
 86                 mirror_to=None, *args, **kwargs):
 87        """
 88        ```
 89        quiet - if type checking should log type errors or operate silently.
 90
 91        Two additional arguments govern how records will be encoded/decoded
 92        from bytes, if desired by the Writer subclass when it calls
 93        _encode_str() or _decode_bytes:
 94
 95        encoding - 'utf-8' by default. If empty or None, do not attempt any
 96                decoding and return raw bytes. Other possible encodings are
 97                listed in online documentation here:
 98                https://docs.python.org/3/library/codecs.html#standard-encodings
 99
100        encoding_errors - 'ignore' by default. Other error strategies are
101                'strict', 'replace', and 'backslashreplace', described here:
102                https://docs.python.org/3/howto/unicode.html#encodings
103
104        mirror_to - Optional Writer to which all records read or transformed
105                by this module (if it is a Reader or Transform) will be
106                "mirrored" (copied). Mirroring happens asynchronously via
107                a queue and background thread to minimize impact on the
108                primary data flow. Writers cannot be mirrored.
109        ```
110        """
111        if kwargs.get('input_format'):
112            logging.warning(f'Code warning: {self.__class__.__name__} use of "input_format"'
113                            'is deprecated in favor of type hints. Please see documentation'
114                            'in logger/utils/base_module.py.')
115        if kwargs.get('output_format'):
116            logging.warning(f'Code warning: {self.__class__.__name__} use of "output_format"'
117                            'is deprecated in favor of type hints. Please see documentation'
118                            'in logger/utils/base_module.py.')
119        self.quiet = quiet
120
121        # Make sure '' behaves the same as None, which is what all the
122        # docstrings say, and would be logical... but then certain things treat
123        # them differently (e.g., file.open(mode='ab', encoding='') throws
124        # ValueError: binary mode doesn't take an encoding argument)
125        if encoding == '':
126            encoding = None
127        self.encoding = encoding
128        self.encoding_errors = encoding_errors
129
130        # Handle mirroring
131        self.mirror_to = mirror_to
132        if self.mirror_to:
133            from logger.writers.writer import Writer
134            if isinstance(self, Writer):
135                logging.warning(f'Writer {self.__class__.__name__} passed "mirror_to" argument. '
136                                'Writers cannot be mirrored.')
137                self.mirror_to = None
138            elif not isinstance(self.mirror_to, Writer):
139                raise TypeError(f'mirror_to must be a Writer, not {type(self.mirror_to)}')
140            else:
141                self.mirror_queue = queue.Queue()
142                self.mirror_thread = threading.Thread(target=self._mirror_output_thread,
143                                                      daemon=True)
144                self.mirror_thread.start()
145
146                # Dynamically wrap the read() or transform() method
147                if hasattr(self, 'read'):
148                    self._original_read = self.read
149                    self.read = self._wrapped_read
150                elif hasattr(self, 'transform'):
151                    self._original_transform = self.transform
152                    self.transform = self._wrapped_transform
quiet - if type checking should log type errors or operate silently.

Two additional arguments govern how records will be encoded/decoded
from bytes, if desired by the Writer subclass when it calls
_encode_str() or _decode_bytes:

encoding - 'utf-8' by default. If empty or None, do not attempt any
        decoding and return raw bytes. Other possible encodings are
        listed in online documentation here:
        https://docs.python.org/3/library/codecs.html#standard-encodings

encoding_errors - 'ignore' by default. Other error strategies are
        'strict', 'replace', and 'backslashreplace', described here:
        https://docs.python.org/3/howto/unicode.html#encodings

mirror_to - Optional Writer to which all records read or transformed
        by this module (if it is a Reader or Transform) will be
        "mirrored" (copied). Mirroring happens asynchronously via
        a queue and background thread to minimize impact on the
        primary data flow. Writers cannot be mirrored.
quiet
encoding
encoding_errors
mirror_to
def can_process_record(self, record):
204    def can_process_record(self, record):
205        """ Is this record in a format that the transform or writer can handle?
206
207        - If there are type hints: True if type of record is in type hints.
208        - If there are no type hints: False if None or list, otherwise True.
209
210        The logic is that if there are no type hints and we see False or
211        a list, we expect digest_record() to be called to deal with it."""
212
213        try:  # if we've not been initialized with type hints, initialize now
214            self.initialized or True
215        except AttributeError:
216            # This will call the subclass initialization, e.g.
217            # Transform._initialize_type_hints(), which will in turn call
218            # OpenRVDASModule._initialize_type_hints()
219            self._initialize_type_hints()
220
221        # Special case: we want to turn empty str records into None. By saying
222        # no, record should get punted to digest_record(), which will do the
223        # right thing.
224        if isinstance(record, str) and not len(record):
225            return False
226
227        if self.input_types:
228            return isinstance(record, self.input_types)
229
230        # If not type hints, make some judgment calls. Say "no" to None
231        # and to lists, because we'll expect that answer to trigger a
232        # call to digest_record(), which will handle them.
233        if record is None or isinstance(record, list):
234            return False
235        return True

Is this record in a format that the transform or writer can handle?

  • If there are type hints: True if type of record is in type hints.
  • If there are no type hints: False if None or list, otherwise True.

The logic is that if there are no type hints and we see False or a list, we expect digest_record() to be called to deal with it.

def digest_record(self, record):
238    def digest_record(self, record):
239        """ Try to digest record down into a format that the method can
240        handle. Typically that will mean that we've been handed a list of
241        records that we need to break into individual records."""
242
243        try:  # if we've not been initialized with type hints, initialize now
244            self.initialized or True
245        except AttributeError:
246            self._initialize_type_hints()
247
248        # Go through our litany of things that reduce to None
249        if record is None:
250            return None
251
252        if isinstance(record, str) and not len(record):
253            return None
254
255        # If it's a type the method can handle directly (though, if so,
256        # why were we called?!?
257        if self.can_process_record(record) and not self.quiet:
258            logging.warning(f'{self.class_name}: digest_record() called unnecessarily.')
259            logging.warning(f'Can process {self.input_types}; received {type(record)}: {record}')
260            return self.module_method(record)
261
262        # We know how to deal with it if it's a list: Apply to components,
263        # stripping out any None's
264        if isinstance(record, list):
265            result = [self.module_method(self, r) for r in record if r is not None]
266            return [r for r in result if r is not None]  # remove Nones
267
268        # Is record a number we can convert to a string?
269        if str in self.input_types and isinstance(record, (int, float)):
270            return str(record)
271
272        # If it's a DASRecord, serialize it as JSON
273        if str in self.input_types and isinstance(record, DASRecord):
274            return record.as_json()
275
276        # If we don't know how to deal with it
277        if not self.quiet:
278            logging.warning(f'Unable to convert record to format "{self.class_name}" can process')
279            logging.warning(f'Must be instance or list of {self.input_types}')
280            logging.warning(f'Received {type(record)}: {record}')
281        return None

Try to digest record down into a format that the method can handle. Typically that will mean that we've been handed a list of records that we need to break into individual records.