From c7107c2a70105a391763721cb25edd68f4b7024f Mon Sep 17 00:00:00 2001 From: Théo de la Hogue Date: Thu, 18 Jan 2024 16:32:57 +0100 Subject: Formatting tuple or list elements. --- src/argaze/ArFeatures.py | 4 +- src/argaze/DataFeatures.py | 91 ++++++++++++---------- src/argaze/DataLog/File.py | 25 ++++-- .../utils/demo_data/demo_gaze_analysis_setup.json | 14 ++-- 4 files changed, 81 insertions(+), 53 deletions(-) diff --git a/src/argaze/ArFeatures.py b/src/argaze/ArFeatures.py index e7b94c0..faa3d9a 100644 --- a/src/argaze/ArFeatures.py +++ b/src/argaze/ArFeatures.py @@ -459,7 +459,7 @@ class ArLayer(DataFeatures.SharedObject): # Log look data for logger_module_path, logger in self.loggers.items(): - logger.emit(timestamp, DataFeatures.DataDictionary(look_data)) + logger(timestamp, DataFeatures.DataDictionary(look_data)) # Unlock layer exploitation self.release() @@ -979,7 +979,7 @@ class ArFrame(DataFeatures.SharedObject): # Log look data for logger_module_path, logger in self.loggers.items(): - logger.emit(timestamp, DataFeatures.DataDictionary(look_data)) + logger(timestamp, DataFeatures.DataDictionary(look_data)) # Unlock frame exploitation self.release() diff --git a/src/argaze/DataFeatures.py b/src/argaze/DataFeatures.py index cf7566a..ac3000c 100644 --- a/src/argaze/DataFeatures.py +++ b/src/argaze/DataFeatures.py @@ -7,7 +7,7 @@ __credits__ = [] __copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" __license__ = "BSD" -from typing import TypeVar, Tuple +from typing import TypeVar, Tuple, Any from dataclasses import dataclass, field import importlib from inspect import getmembers @@ -103,13 +103,6 @@ class JsonEncoder(json.JSONEncoder): return public_dict -class DataDictionary(dict): - """Enable dot.notation access to dictionary attributes""" - - __getattr__ = dict.get - __setattr__ = dict.__setitem__ - __delattr__ = dict.__delitem__ - class SharedObject(): """Enable multiple threads sharing.""" @@ -180,37 +173,6 @@ class SharedObject(): self._token = token self._lock.release() -TimeStampedDataLoggerType = TypeVar('TimeStampedDataLogger', bound="TimeStampedDataLogger") -# Type definition for type annotation convenience - -@dataclass -class TimeStampedDataLogger(): - """Abstract class to define what should provide a timestamped data logger.""" - - selector: str = field(default='data') - """Code evaluated to select data. Default 'data' string means that all incoming data will be written.""" - - @classmethod - def from_dict(self, logger_module_path: str, logger_parameters: dict) -> TimeStampedDataLoggerType: - """Load timestamped data logger from dictionary. - - Parameters: - logger_module_path: class name to load - logger_parameters: attributes to load - """ - - # Prepend argaze.DataLog path when a single name is provided - if len(logger_module_path.split('.')) == 1: - logger_module_path = f'argaze.DataLog.{logger_module_path}' - - logger_module = importlib.import_module(logger_module_path) - return logger_module.TimeStampedDataLogger(**logger_parameters) - - def emit(self, timestamp: TimeStampType, data: DataDictionary): - """Emit timestamped data dictionary to a specific log destination.""" - - raise NotImplementedError('emit() method not implemented') - class TimeStampedBuffer(collections.OrderedDict): """Ordered dictionary to handle timestamped data. ``` @@ -458,3 +420,54 @@ class TimeStampedBuffer(collections.OrderedDict): legend_patches.append(mpatches.Patch(color=color, label=name.upper())) return legend_patches + +class DataDictionary(dict): + """Enable dot.notation access to dictionary attributes""" + + __getattr__ = dict.get + __setattr__ = dict.__setitem__ + __delattr__ = dict.__delitem__ + +# Import libraries that can be used in selector or formatter codes +from argaze import GazeFeatures + +TimeStampedDataLoggerType = TypeVar('TimeStampedDataLogger', bound="TimeStampedDataLogger") +# Type definition for type annotation convenience + +@dataclass +class TimeStampedDataLogger(): + """Abstract class to define what should provide a timestamped data logger.""" + + selector: str = field(default='True') + """Code evaluated to select data under a condition. Default 'True' string means that all incoming data will be accepted.""" + + formatter: str|list = field(default='timestamp, data') + """Code (or list of codes) evaluated to format timestamp and data to emit. Default 'timestamp, data' string means that all incoming timestamp and data values will be emitted.""" + + @classmethod + def from_dict(self, logger_module_path: str, logger_parameters: dict) -> TimeStampedDataLoggerType: + """Load timestamped data logger from dictionary. + + Parameters: + logger_module_path: class name to load + logger_parameters: attributes to load + """ + + # Prepend argaze.DataLog path when a single name is provided + if len(logger_module_path.split('.')) == 1: + logger_module_path = f'argaze.DataLog.{logger_module_path}' + + logger_module = importlib.import_module(logger_module_path) + return logger_module.TimeStampedDataLogger(**logger_parameters) + + def __call__(self, timestamp: TimeStampType, data: DataDictionary) -> Any: + """Apply selector code to decide if data have to be logged, then apply formatter code before to call specific logger emit method.""" + + if eval(self.selector): + + return self.emit(eval(self.formatter)) + + def emit(self, formatted_log: any): + """Emit formatted log to a destination.""" + + raise NotImplementedError('handle() method not implemented') diff --git a/src/argaze/DataLog/File.py b/src/argaze/DataLog/File.py index 12bad6d..8179f8f 100644 --- a/src/argaze/DataLog/File.py +++ b/src/argaze/DataLog/File.py @@ -12,7 +12,7 @@ from typing import TypeVar, Tuple from dataclasses import dataclass, field import os, pathlib -from argaze import DataFeatures, GazeFeatures +from argaze import DataFeatures @dataclass class TimeStampedDataLogger(DataFeatures.TimeStampedDataLogger): @@ -21,8 +21,11 @@ class TimeStampedDataLogger(DataFeatures.TimeStampedDataLogger): path: str = field(default=None) """File path where to write data.""" - separator: chr = field(default=' ') - """Char used to separate timestamp from data""" + header: str = field(default=None) + """String to write first.""" + + separator: str = field(default=", ") + """String used to separate list or tuple formatted log.""" def __post_init__(self): """Check that folder structure exist and create file.""" @@ -35,13 +38,23 @@ class TimeStampedDataLogger(DataFeatures.TimeStampedDataLogger): # Open file self._file = open(self.path, 'w', encoding='utf-8', buffering=1) + # Write header if required + if self.header is not None: + + print(self.header, file=self._file, flush=True) + def __del__(self): """Close file.""" self._file.close() - def emit(self, timestamp: DataFeatures.TimeStampType, data: DataFeatures.DataDictionary): - """Write timestamp and data separated by separator char as a new line into file.""" + def emit(self, formatted_log: any): + """Write log as a new line into file. List or tuple are converted into strings separated by separator char.""" + + # Format list or tuple element into quoted strings + if not isinstance(formatted_log, str): + + formatted_log = self.separator.join(f'\"{d}\"' for d in formatted_log) # Write into file - print(f'{timestamp}{self.separator}{eval(self.selector)}', file=self._file, flush=True) \ No newline at end of file + print(formatted_log, file=self._file, flush=True) \ No newline at end of file diff --git a/src/argaze/utils/demo_data/demo_gaze_analysis_setup.json b/src/argaze/utils/demo_data/demo_gaze_analysis_setup.json index 38db16f..39eaeca 100644 --- a/src/argaze/utils/demo_data/demo_gaze_analysis_setup.json +++ b/src/argaze/utils/demo_data/demo_gaze_analysis_setup.json @@ -49,18 +49,20 @@ }, "loggers": { "File" : { - "path": "_export/logs/layer_data.txt", - "separator": ",", - "selector": "data.looked_aoi_name" + "path": "_export/logs/aoi.csv", + "header": "Timestamp, AOI", + "selector": "data.looked_aoi_name is not None", + "formatter": "timestamp, data.looked_aoi_name" } } } }, "loggers": { "File" : { - "path": "_export/logs/frame_data.txt", - "separator": ",", - "selector": "data.identified_gaze_movement.focus if GazeFeatures.is_fixation(data.identified_gaze_movement) else None" + "path": "_export/logs/fixations.csv", + "header": "Timestamp, Focus, Duration", + "selector": "GazeFeatures.is_fixation(data.identified_gaze_movement)", + "formatter": "timestamp, data.identified_gaze_movement.focus, data.identified_gaze_movement.duration" } }, "image_parameters": { -- cgit v1.1