From 47234c7dbf5d064fa2f6fa4c50939a60a2da2cbd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Romain=20Bron=C3=A8s?= <romain.brones@synchrotron-soleil.fr>
Date: Tue, 6 Feb 2024 09:25:51 +0100
Subject: [PATCH] On the way for module style

---
 ArchiveExtractor.py           | 857 ----------------------------------
 ArchiveExtractor/Access.py    | 274 +++++++++++
 ArchiveExtractor/Amenities.py | 269 +++++++++++
 ArchiveExtractor/Core.py      | 314 +++++++++++++
 ArchiveExtractor/__init__.py  |   7 +
 __init__.py                   |   0
 6 files changed, 864 insertions(+), 857 deletions(-)
 delete mode 100755 ArchiveExtractor.py
 create mode 100644 ArchiveExtractor/Access.py
 create mode 100644 ArchiveExtractor/Amenities.py
 create mode 100644 ArchiveExtractor/Core.py
 create mode 100644 ArchiveExtractor/__init__.py
 delete mode 100644 __init__.py

diff --git a/ArchiveExtractor.py b/ArchiveExtractor.py
deleted file mode 100755
index 6e66415..0000000
--- a/ArchiveExtractor.py
+++ /dev/null
@@ -1,857 +0,0 @@
-"""
-Python module for extracting attribute from Archive Extractor Device.
-"""
-import logging
-import datetime
-import numpy as np
-import PyTango as tango
-import pandas as pd
-import traceback
-
-__version__ = "1.0.1"
-
-##########################################################################
-###                 Install logger for the module                      ###
-##########################################################################
-logger = logging.getLogger(__name__)
-#logger.setLevel(getattr(logging, logger.upper()))
-
-if not logger.hasHandlers():
-    # No handlers, create one
-    sh = logging.StreamHandler()
-    sh.setLevel(logger.level)
-    sh.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))
-    logger.addHandler(sh)
-
-
-##########################################################################
-###               Commodity private variables                          ###
-##########################################################################
-
-# Extractor date format for GetAttDataBetweenDates
-_DBDFMT = "%Y-%m-%d %H:%M:%S"
-
-# Extractor date format for GetNearestValue
-_DBDFMT2 = "%d-%m-%Y %H:%M:%S"
-
-##########################################################################
-###               Commodity private functions                          ###
-##########################################################################
-
-# Vectorized fromtimestamp function
-# NOTE: it is faster than using pandas.to_datetime()
-_ArrayTimeStampToDatetime = np.vectorize(datetime.datetime.fromtimestamp)
-
-# Vectorized bool map dictionnary
-_ArrayStr2Bool = np.vectorize({
-    "true":True, 't':True,
-    "false":False, 'f':False,
-    }.get)
-
-
-def _check_initialized():
-    """
-    Check if the module is initialized.
-
-    Returns
-    -------
-    success : boolean
-    """
-    global _extractors
-    if None in _extractors:
-        logger.error("Module {0} is not initialied. You should run {0}.init().".format(__name__))
-        return False
-    return True
-
-##----------------------------------------------------------------------##
-def _dateparse(datestr):
-    """
-    Convenient function to parse date or duration strings.
-    Exact date format is %Y-%m-%d-%H:%M:%S and it can be reduced to be less precise.
-    Duration format is 'Xu' where X is a number and u is a unit in ('m':minutes, 'h':hours, 'd':days, 'M':months)
-    If datstr is None, take the actual date and time.
-
-    Parameters
-    ---------
-    datestr : string
-        Date as a string, format %Y-%m-%d-%H:%M:%S or less precise.
-        Duration as a string, format 'Xu' where X is a number and u is a unit in ('m':minutes, 'h':hours, 'd':days, 'M':months)
-
-    Exceptions
-    ----------
-    ValueError
-        If the parsing failed.
-
-    Returns
-    -------
-    date : datetime.datetime or datetime.timedelta
-        Parsed date or duration
-    """
-    logger.debug("Parsing date string '%s'"%datestr)
-
-    # Determine date/duration by looking at the last char
-    if datestr[-1] in "mhdM":
-        # Duration
-        logger.debug("Assuming a duration")
-
-        try:
-            q=float(datestr[:-1])
-        except ValueError as e:
-            logger.error("Failed to parse date string. Given the last character, a duration was assumed.")
-            raise Exception("Could not parse argument to a date") from e
-
-        # Convert all in minutes
-        minutes = q*{'m':1, 'h':60, 'd':60*24, 'm':30*60*24}[datestr[-1]]
-
-        return datetime.timedelta(minutes=minutes)
-
-    else:
-        # Probably a date string
-
-        # This gives all format that will be tried, in order.
-        # Stop on first parse success. Raise error if none succeed.
-        fmt = [
-            "%Y-%m-%d-%H:%M:%S",
-            "%Y-%m-%d-%H:%M",
-            "%Y-%m-%d-%H",
-            "%Y-%m-%d",
-            "%Y-%m",
-            ]
-
-        date = None
-        for f in fmt:
-            try:
-                date = datetime.datetime.strptime(datestr, f)
-            except ValueError:
-                continue
-            else:
-                break
-        else:
-            raise ValueError("Could not parse argument to a date")
-
-        return date
-
-##----------------------------------------------------------------------##
-def _check_attribute(attribute, db):
-    """
-    Check that the attribute is in the database
-
-    Parameters
-    ----------
-    attribute : String
-        Name of the attribute. Full Tango name i.e. "test/dg/panda/current".
-
-    db: str
-        Which database to look in, 'H' or 'T'.
-    """
-    global _extractors
-
-    logger.debug("Check that %s is archived."%attribute)
-    if not _extractors[{'H':0, 'T':1}[db]].IsArchived(attribute):
-        logger.error("Attribute '%s' is not archived in DB %s"%(attribute, _extractors[{'H':0, 'T':1}[db]]))
-        raise ValueError("Attribute '%s' is not archived in DB %s"%(attribute, _extractors[{'H':0, 'T':1}[db]]))
-
-##----------------------------------------------------------------------##
-def _chunkerize(attribute, dateStart, dateStop, db, Nmax=100000):
-    """
-
-    Parameters
-    ----------
-    attribute : String
-        Name of the attribute. Full Tango name i.e. "test/dg/panda/current".
-
-    dateStart : datetime.datetime
-        Start date for extraction.
-
-    dateStop : datetime.datetime
-        Stop date for extraction.
-
-    db: str
-        Which database to look in, 'H' or 'T'.
-
-    Nmax: int
-        Max number of atoms in one chunk. Default 100000.
-
-    Returns
-    -------
-    cdates : list
-        List of datetime giving the limit of each chunks.
-        For N chunks, there is N+1 elements in cdates, as the start and end boundaries are included.
-    """
-    info=infoattr(attribute, db=db)
-    logger.debug("Attribute information \n%s"%info)
-
-    # Get the number of points
-    N=_extractors[{'H':0, 'T':1}[db]].GetAttDataBetweenDatesCount([
-            attribute,
-            dateStart.strftime(_DBDFMT2),
-            dateStop.strftime(_DBDFMT2)
-            ])
-    logger.debug("On the period, there is %d entries"%N)
-
-    dx=int(info["max_dim_x"])
-    if dx > 1:
-        logger.debug("Attribute is a vector with max dimension = %s"%dx)
-        N=N*dx
-
-    # If data chunk is too much, we need to cut it
-    if N > Nmax:
-        dt = (dateStop-dateStart)/(N//Nmax)
-        cdates = [dateStart]
-        while cdates[-1] < dateStop:
-            cdates.append(cdates[-1]+dt)
-        cdates[-1] = dateStop
-        logger.debug("Cutting access to %d little chunks of time, %s each."%(len(cdates)-1, dt))
-    else:
-        cdates=[dateStart, dateStop]
-
-    return cdates
-
-##----------------------------------------------------------------------##
-def _cmd_with_retry(dp, cmd, arg, retry=2):
-    """
-    Run a command on tango.DeviceProxy, retrying on DevFailed.
-
-    Parameters
-    ----------
-    dp: tango.DeviceProxy
-        Device proxy to try command onto.
-
-    cmd : str
-        Command to executte on the extractor
-
-    arg : list
-        Attribute to pass to the command
-
-    retry : int
-        Number of command retry on DevFailed
-
-    Returns
-    -------
-    cmdreturn :
-        Whatever the command returns.
-        None if failed after the amount of retries.
-    """
-    logger.info("Perform Command {} {}".format(cmd, arg))
-
-    for i in range(retry):
-        # Make retrieval request
-        logger.debug("Execute %s (%s)"%(cmd, arg))
-        try:
-            cmdreturn = getattr(dp, cmd)(arg)
-        except tango.DevFailed as e:
-            logger.warning("The extractor device returned the following error:")
-            logger.warning(e)
-            if  i == retry-1:
-                logger.error("Could not execute command %s (%s). Check the device extractor"%(cmd, arg))
-                return None
-            logger.warning("Retrying...")
-            continue
-        break
-    return cmdreturn
-
-
-def _cast_bool(value):
-    """
-    Cast a value, or array of values, to boolean.
-    Try to assess the input data type. If string, then try to find true or false word inside.
-
-    Parameters:
-    -----------
-    value: string, integer, or array of such
-        value to convert.
-
-    Return:
-    boolean:
-        value or array of boolean.
-    """
-
-    # Force to array
-    value = np.asarray(value)
-
-    # cast back to single value
-    def castback(v):
-        if v.shape == ():
-            return v.item()
-        return v
-
-    # Simply try to cast to bool first
-    try:
-        value = value.astype("bool")
-        logger.debug("Direct conversion to boolean")
-        return castback(value)
-    except ValueError:
-        # Keep trying to cast
-        pass
-
-    logger.debug("Try to convert to boolean")
-
-    value = np.char.strip(np.char.lower(value))
-    value = _ArrayStr2Bool(value)
-
-    return castback(value)
-
-
-##########################################################################
-###                  Module private variables                          ###
-##########################################################################
-# Tuple of extractor for HDB and TDB
-_extractors = (None, None)
-
-# Tuple for attribute tables
-_AttrTables = (None, None)
-
-##########################################################################
-###                Module initialisation functions                     ###
-##########################################################################
-
-def init(
-        HdbExtractorPath="archiving/hdbextractor/2",
-        TdbExtractorPath="archiving/tdbextractor/2",
-        loglevel="info",
-            ):
-    """
-    Initialize the module.
-    Instanciate tango.DeviceProxy for extractors (TDB and HDB)
-
-    Parameters:
-    -----------
-    HdbExtractorPath, TdbExtractorPath: string
-        Tango path to the extractors.
-
-    loglevel: string
-        loglevel to pass to logging.Logger
-    """
-    global _extractors
-    global _AttrTables
-
-    _extractors = (None, None)
-    _AttrTables = (None, None)
-
-    try:
-        logger.setLevel(getattr(logging, loglevel.upper()))
-    except AttributeError:
-        logger.error("Wrong log level specified: {}".format(loglevel.upper()))
-
-    logger.debug("Instanciating extractors device proxy...")
-
-    _extractors = (tango.DeviceProxy(HdbExtractorPath), tango.DeviceProxy(TdbExtractorPath))
-    logger.debug("{} and {} instanciated.".format(*_extractors))
-
-    logger.debug("Configuring extractors device proxy...")
-    for e in _extractors:
-        # set timeout to 3 sec
-        e.set_timeout_millis(3000)
-
-    logger.debug("Filling attributes lookup tables...")
-    _AttrTables = tuple(e.getattnameall() for e in _extractors)
-    logger.debug("HDB: {} TDB: {} attributes counted".format(len(_AttrTables[0]), len(_AttrTables[1])))
-
-##########################################################################
-###                    Module access functions                         ###
-##########################################################################
-
-def extract(
-        attr,
-        date1, date2=None,
-        method="nearest",
-        db='H',
-        ):
-    """
-    Access function to perform extraction between date1 and date2.
-    Can extract one or several attributes.
-    date1 and date2 can be both exact date, or one of two can be a time interval that will be taken relative to the other.
-
-
-    Parameters:
-    -----------
-    attr: string, list, dict
-        Attribute(s) to extract.
-        If string, extract the given attribute, returning a pandas.Series.
-        If list, extract attributes and return a list of pandas.Series.
-        If a dict, extract attributes and return a dict of pandas.Series with same keys.
-
-    date1, date2: string, datetime.datetime, datetime.timedelta, None
-        Exact date, or duration relative to date2.
-        If string, it will be parsed.
-        A start date can be given with string format '%Y-%m-%d-%H:%M:%S' or less precise (ie '2021-02', '2022-11-03' '2022-05-10-21:00'.i..).
-        A duration can be given with string format 'Xu' where X is a number and u is a unit in ('m':minutes, 'h':hours, 'd':days, 'M':months)
-        A datetime.datetime object or datetime.timedelta object will be used as is.
-        date2 can be None. In that case it is replaced by the current time.
-
-    method: str
-        Method of extraction
-            'nearest': Retrieve nearest value of date1, date2 is ignored.
-            'between': Retrive data between date1 and date2.
-
-    db: str
-        Which database to look in, 'H' or 'T'.
-
-    """
-
-    ## _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
-    #    Perform a few sanity checks
-    if not _check_initialized():
-        # Stop here, the function has produced a message if necessary
-        return
-
-    if not db in ("H", "T"):
-        raise ValueError("Attribute 'db' should be 'H' or 'T'")
-
-
-    allowedmethods=("nearest", "between", "minmaxmean")
-    if not method in allowedmethods:
-        raise ValueError("Attribute 'method' should be in {}".format(str(allowedmethods)))
-
-    ## _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
-    #     Work with dates
-    if not type(date1) in (datetime.datetime, datetime.timedelta):
-        date1 = _dateparse(date1)
-    if date2 is None:
-        date2 = datetime.datetime.now()
-    else:
-        if not type(date2) in (datetime.datetime, datetime.timedelta):
-            date2 = _dateparse(date2)
-
-    if not datetime.datetime in (type(date1), type(date2)):
-        logger.error("One of date1 date2 should be an exact date.\nGot {} {}".format(date1, date2))
-        raise ValueError("date1 and date2 not valid")
-
-    # Use timedelta relative to the other date. date1 is always before date2
-    if type(date1) is datetime.timedelta:
-        date1 = date2-date1
-    if type(date2) is datetime.timedelta:
-        date2 = date1+date2
-
-    if  date1 > date2:
-        logger.error("date1 must precede date2.\nGot {} {}".format(date1, date2))
-        raise ValueError("date1 and date2 not valid")
-
-    ## _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
-    #      Perform extraction and return
-
-    if type(attr) is dict:
-        d=dict()
-        for k,v in attr.items():
-            try:
-                d.update({k:_extract_attribute(v, method, date1, date2, db)})
-            except Exception as e:
-                logger.debug("Exception in _extract_attribute(): "+str(e))
-                logger.debug(traceback.print_tb(e.__traceback__))
-                logger.error("Could not extract {}.".format(v))
-
-        return d
-
-    if type(attr) in (list,tuple):
-        d=[]
-        for v in attr:
-            try:
-                d.append(_extract_attribute(v, method, date1, date2, db))
-            except Exception as e:
-                logger.debug("Exception in _extract_attribute(): "+str(e))
-                logger.debug(traceback.print_tb(e.__traceback__))
-                logger.error("Could not extract {}.".format(v))
-
-        return d
-
-    try:
-        d=_extract_attribute(attr, method, date1, date2, db)
-    except Exception as e:
-        logger.debug("Exception in _extract_attribute(): "+str(e))
-        logger.debug(traceback.print_tb(e.__traceback__))
-        logger.error("Could not extract {}.".format(attr))
-        return None
-
-    return d
-
-
-##----------------------------------------------------------------------##
-def findattr(pattern, db="H"):
-    """
-    Search for an attribute path using the pattern given.
-    Case insensitive.
-
-    Parameters:
-    -----------
-    pattern: str
-        Pattern to search, wildchar * accepted.
-        example "dg*dcct*current"
-
-    db: str
-        Which database to look in, 'H' or 'T'.
-
-    Returns:
-    --------
-    results: (str,)
-        List of string match
-    """
-    if not _check_initialized():
-        return
-
-    if not db in ("H", "T"):
-        raise AttributeError("Attribute db should be 'H' or 'T'")
-
-    global _AttrTables
-
-    keywords=pattern.lower().split('*')
-
-    # Select DB
-    attr_table = _AttrTables[{'H':0, 'T':1}[db]]
-
-    matches = [attr for attr in attr_table if all(k in attr.lower() for k in keywords)]
-
-    return matches
-
-##----------------------------------------------------------------------##
-def infoattr(attribute, db='H'):
-    """
-    Get informations for an attribute and pack it into a python dict.
-
-    Parameters
-    ----------
-    attribute : String
-        Name of the attribute. Full Tango name i.e. "test/dg/panda/current".
-
-    db: str
-        Which database to look in, 'H' or 'T'.
-
-    Returns
-    -------
-    info : dict
-        Dictionnary of propertyname:propertyvalue
-    """
-    if not _check_initialized():
-        return
-
-    if not db in ("H", "T"):
-        raise AttributeError("Attribute db should be 'H' or 'T'")
-
-    info = dict()
-
-    for func in ("GetAttDefinitionData", "GetAttPropertiesData"):
-        R=getattr(_extractors[{'H':0, 'T':1}[db]], func)(attribute)
-        if not R is None:
-            for i in R:
-                _s=i.split("::")
-                info[_s[0]]=_s[1]
-        else:
-            logger.warning("Function %s on extractor returned None"%func)
-
-    return info
-
-##########################################################################
-###                    Module core functions                           ###
-##########################################################################
-
-def _extract_attribute(attribute, method, date1, date2, db):
-    """
-    Check if exists, check scalar or spectrum and dispatch
-    """
-
-    # Uncapitalize attribute
-    attribute = attribute.lower()
-    _check_attribute(attribute, db)
-
-    # Get info about the attribute
-    info=infoattr(attribute, db=db)
-    logger.debug("Attribute information \n%s"%info)
-
-    # Detect spectrum
-    attrtype="scalar"
-    if int(info["max_dim_x"]) > 1:
-        if int(info["max_dim_y"]) > 0:
-            logger.warning("Attribute %s is a (%s; %s) vector. This is poorly handled by this module."%(
-                attribute, info["max_dim_x"], info["max_dim_y"]))
-            attrtype="multi"
-        else:
-            logger.info("Attribute %s is a 1D vector, dimension = %s."%(
-                attribute, info["max_dim_x"]))
-            attrtype="vector"
-
-    # =============
-    # For now we handle multi dimension the same way as scalar, which will get only the first element
-    if (attrtype=="scalar") or (attrtype=="multi"):
-        if info["data_type"] == '1':
-            # Boolean data type, quick fix
-            dtype=bool
-        else:
-            dtype=float
-
-        return _extract_scalar(attribute, method, date1, date2, db, dtype)
-    if attrtype=="vector":
-        return _extract_vector(attribute, method, date1, date2, db)
-
-
-##---------------------------------------------------------------------------##
-def _extract_scalar(attribute, method, date1, date2, db, dtype):
-
-    # =====================
-    if method == "nearest":
-        cmdreturn = _cmd_with_retry(_extractors[{'H':0, 'T':1}[db]], "GetNearestValue", [
-                                                attribute,
-                                                date1.strftime(_DBDFMT),
-                                                ])
-
-        # Unpack return
-        try:
-            _date, _value = cmdreturn.split(';')
-        except TypeError:
-            logger.error("Could not extract this chunk. Check the device extractor")
-            return None
-
-        # Transform by datatype
-        if dtype is bool:
-            _value = _cast_bool(_value)
-
-        # Fabricate return pandas.Series
-        d=pd.Series(index=[datetime.datetime.fromtimestamp(int(_date)/1000),], data=[_value,], name=attribute)
-
-        return d
-
-    # =====================
-    if method == "between":
-        # Cut the time horizon in chunks
-        cdates = _chunkerize(attribute, date1, date2, db)
-
-        # Array to hold data
-        data = []
-
-        # For each date chunk
-        for i_d in range(len(cdates)-1):
-            cmdreturn = _cmd_with_retry(_extractors[{'H':0, 'T':1}[db]], "ExtractBetweenDates", [
-                                                    attribute,
-                                                    cdates[i_d].strftime(_DBDFMT),
-                                                    cdates[i_d+1].strftime(_DBDFMT)
-                                                    ])
-
-
-            # Unpack return
-            try:
-                _date, _value = cmdreturn
-            except TypeError:
-                logger.error("Could not extract this chunk. Check the device extractor")
-                return None
-
-
-            # Transform to datetime - value arrays
-            if dtype is bool:
-                _value = _cast_bool(_value)
-            else:
-                _value = np.asarray(_value, dtype=dtype)
-
-            if len(_date) > 0:
-                _date = _ArrayTimeStampToDatetime(_date/1000.0)
-
-            # Fabricate return pandas.Series
-            data.append(pd.Series(index=_date, data=_value, name=attribute))
-
-        # Concatenate chunks
-        return pd.concat(data)
-
-    # ========================
-    if method == "minmaxmean":
-
-        # If we are here, the method is not implemented
-        logger.error("Method {} is not implemented for scalars.".format(method))
-        raise NotImplemented
-
-##---------------------------------------------------------------------------##
-def _extract_vector(attribute, method, date1, date2, db):
-
-    # Get info about the attribute
-    info=infoattr(attribute, db=db)
-
-    # =====================
-    if method == "nearest":
-        # Get nearest does not work with vector.
-        # Make a between date with surounding dates.
-
-        # Dynamically find surounding
-        cnt=0
-        dt=datetime.timedelta(seconds=10)
-        while cnt<1:
-            logger.debug("Seeking points in {} to {}".format(date1-dt,date1+dt))
-            cnt=_extractors[{'H':0, 'T':1}[db]].GetAttDataBetweenDatesCount([
-                    attribute,
-                    (date1-dt).strftime(_DBDFMT2),
-                    (date1+dt).strftime(_DBDFMT2)
-                    ])
-            dt=dt*1.5
-        logger.debug("Found {} points in a +- {} interval".format(cnt,str(dt/1.5)))
-
-
-        # For vector, we have to use the GetAttxxx commands
-        cmdreturn = _cmd_with_retry(_extractors[{'H':0, 'T':1}[db]], "GetAttDataBetweenDates", [
-                                                attribute,
-                                                (date1-dt).strftime(_DBDFMT),
-                                                (date1+dt).strftime(_DBDFMT),
-                                                ])
-
-        # Unpack return
-        try:
-            [N,], [name,] = cmdreturn
-            N=int(N)
-        except TypeError:
-            logger.error("Could not extract this attribute. Check the device extractor")
-            return None
-
-        # Read the history
-        logger.debug("Retrieve history of %d values. Dynamic attribute named %s."%(N, name))
-        attrHist = _extractors[{'H':0, 'T':1}[db]].attribute_history(name, N)
-
-        # Transform to datetime - value arrays
-        _value = np.empty((N, int(info["max_dim_x"])), dtype=float)
-        _value[:] = np.nan
-        _date = np.empty(N, dtype=object)
-        for i_h in range(N):
-            _value[i_h,:attrHist[i_h].dim_x]=attrHist[i_h].value
-            _date[i_h]=attrHist[i_h].time.todatetime()
-
-        # Seeking nearest entry
-        idx=np.argmin(abs(_date-date1))
-        logger.debug("Found nearest value at index {}: {}".format(idx, _date[idx]))
-
-        # Fabricate return pandas.Series
-        d=pd.Series(index=[_date[idx],], data=[_value[idx],], name=attribute)
-
-        return d
-
-    # If we are here, the method is not implemented
-    logger.error("Method {} is not implemented for vectors.".format(method))
-    raise NotImplemented
-
-
-##---------------------------------------------------------------------------##
-def ExtrBetweenDates_MinMaxMean(
-        attribute,
-        dateStart,
-        dateStop=None,
-        timeInterval=datetime.timedelta(seconds=60),
-        db='H',
-        ):
-    """
-    Query attribute data from an archiver database, get all points between dates.
-    Use ExtractBetweenDates.
-
-    Parameters
-    ----------
-    attribute : String
-        Name of the attribute. Full Tango name i.e. "test/dg/panda/current".
-
-    dateStart : datetime.datetime, string
-        Start date for extraction. If string, it will be parsed.
-        Example of string format %Y-%m-%d-%H:%M:%S or less precise.
-
-    dateStop : datetime.datetime, string
-        Stop date for extraction. If string, it will be parsed.
-        Example of string format %Y-%m-%d-%H:%M:%S or less precise.
-        Default is now (datetime.datetime.now())
-
-    timeInterval: datetime.timedelta, string
-        Time interval used to perform min,max and mean.
-        Can be a string with a number and a unit in "d", "h", "m" or "s"
-
-    db: str
-        Which database to look in, 'H' or 'T'.
-
-    Exceptions
-    ----------
-    ValueError
-        The attribute is not found in the database.
-
-    Returns
-    -------
-    [mdates, value_min, value_max, value_mean] : array
-        mdates : numpy.ndarray of datetime.datime objects
-            Dates of the values, middle of timeInterval windows
-        value_min : numpy.ndarray
-            Minimum of the value on the interval
-        value_max : numpy.ndarray
-            Maximum of the value on the interval
-        value_mean : numpy.ndarray
-            Mean of the value on the interval
-
-    """
-    if not _check_initialized():
-        return
-
-    if not db in ("H", "T"):
-        raise AttributeError("Attribute db should be 'H' or 'T'")
-
-    # Uncapitalize attribute
-    attribute = attribute.lower()
-
-    # Check attribute is in database
-    _check_attribute(attribute, db=db)
-
-    # Parse dates
-    dateStart = _dateparse(dateStart)
-    dateStop = _dateparse(dateStop)
-
-    # Parse timeInterval if string
-    if type(timeInterval) is str:
-        try:
-            mul = {'s':1, 'm':60, 'h':60*60, 'd':60*60*24}[timeInterval[-1]]
-        except KeyError:
-            logger.error("timeInterval could not be parsed")
-            raise ValueError("timeInterval could not be parsed")
-        timeInterval= datetime.timedelta(seconds=int(timeInterval[:-1])*mul)
-
-    # Get info about the attribute
-    info=infoattr(attribute)
-    logger.debug("Attribute information \n%s"%info)
-
-    # Detect spectrum
-    attrtype="scalar"
-    if int(info["max_dim_x"]) > 1:
-        logger.error("Attribute is not a scalar. Cannot perform this kind of operation.")
-        return None
-
-    # Cut data range in time chunks
-    cdates = [dateStart]
-    while cdates[-1] < dateStop:
-        cdates.append(cdates[-1]+timeInterval)
-    cdates[-1] = dateStop
-    mdates = np.asarray(cdates[:-1])+timeInterval/2
-    logger.debug("Cutting time range to %d chunks of time, %s each."%(len(cdates)-1, timeInterval))
-
-    # Prepare arrays
-    value_min = np.empty(len(cdates)-1)
-    value_max = np.empty(len(cdates)-1)
-    value_mean = np.empty(len(cdates)-1)
-
-    # For each time chunk
-    for i_d in range(len(cdates)-1):
-        for func, arr in zip(
-                ["Max", "Min", "Avg"],
-                [value_max, value_min, value_mean],
-                ):
-            # Make requests
-            logger.debug("Perform GetAttData%sBetweenDates (%s, %s, %s)"%(
-                func,
-                attribute,
-                cdates[i_d].strftime(_DBDFMT2),
-                cdates[i_d+1].strftime(_DBDFMT2))
-                )
-
-            _val =getattr(_extractors[{'H':0, 'T':1}[db]], "GetAttData%sBetweenDates"%func)([
-                attribute,
-                cdates[i_d].strftime(_DBDFMT2),
-                cdates[i_d+1].strftime(_DBDFMT2)
-                ])
-
-            arr[i_d] = _val
-
-    logger.debug("Extraction done for %s."%attribute)
-    return pd.DataFrame(
-            index=mdates,
-            data={
-                "Min":value_min,
-                "Mean":value_mean,
-                "Max":value_max,
-                },)
-
-## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
-## Initialize on import
-## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
-init()
diff --git a/ArchiveExtractor/Access.py b/ArchiveExtractor/Access.py
new file mode 100644
index 0000000..ce376b4
--- /dev/null
+++ b/ArchiveExtractor/Access.py
@@ -0,0 +1,274 @@
+import logging
+import datetime
+import numpy as np
+import tango
+import pandas as pd
+import traceback
+import ArchiveExtractor.Amenities as aea
+import ArchiveExtractor.Core as aec
+
+##########################################################################
+###                 Install logger for the module                      ###
+##########################################################################
+logger = logging.getLogger(__name__)
+#logger.setLevel(getattr(logging, logger.upper()))
+
+if not logger.hasHandlers():
+    # No handlers, create one
+    sh = logging.StreamHandler()
+    sh.setLevel(logger.level)
+    sh.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))
+    logger.addHandler(sh)
+
+
+##########################################################################
+###                  Module private variables                          ###
+##########################################################################
+# Tuple of extractor for HDB and TDB
+_extractors = (None, None)
+
+# Tuple for attribute tables
+_AttrTables = (None, None)
+
+##########################################################################
+###                Module initialisation functions                     ###
+##########################################################################
+
+def init(
+        HdbExtractorPath="archiving/hdbextractor/2",
+        TdbExtractorPath="archiving/tdbextractor/2",
+        loglevel="info",
+            ):
+    """
+    Initialize the module.
+    Instanciate tango.DeviceProxy for extractors (TDB and HDB)
+
+    Parameters:
+    -----------
+    HdbExtractorPath, TdbExtractorPath: string
+        Tango path to the extractors.
+
+    loglevel: string
+        loglevel to pass to logging.Logger
+    """
+    global _extractors
+    global _AttrTables
+
+    _extractors = (None, None)
+    _AttrTables = (None, None)
+
+    try:
+        logger.setLevel(getattr(logging, loglevel.upper()))
+    except AttributeError:
+        logger.error("Wrong log level specified: {}".format(loglevel.upper()))
+
+    logger.debug("Instanciating extractors device proxy...")
+
+    _extractors = (tango.DeviceProxy(HdbExtractorPath), tango.DeviceProxy(TdbExtractorPath))
+    logger.debug("{} and {} instanciated.".format(*_extractors))
+
+    logger.debug("Configuring extractors device proxy...")
+    for e in _extractors:
+        # set timeout to 3 sec
+        e.set_timeout_millis(3000)
+
+    logger.debug("Filling attributes lookup tables...")
+    _AttrTables = tuple(e.getattnameall() for e in _extractors)
+    logger.debug("HDB: {} TDB: {} attributes counted".format(len(_AttrTables[0]), len(_AttrTables[1])))
+
+##########################################################################
+###                    Module access functions                         ###
+##########################################################################
+
+def extract(
+        attr,
+        date1, date2=None,
+        method="nearest",
+        db='H',
+        ):
+    """
+    Access function to perform extraction between date1 and date2.
+    Can extract one or several attributes.
+    date1 and date2 can be both exact date, or one of two can be a time interval that will be taken relative to the other.
+
+
+    Parameters:
+    -----------
+    attr: string, list, dict
+        Attribute(s) to extract.
+        If string, extract the given attribute, returning a pandas.Series.
+        If list, extract attributes and return a list of pandas.Series.
+        If a dict, extract attributes and return a dict of pandas.Series with same keys.
+
+    date1, date2: string, datetime.datetime, datetime.timedelta, None
+        Exact date, or duration relative to date2.
+        If string, it will be parsed.
+        A start date can be given with string format '%Y-%m-%d-%H:%M:%S' or less precise (ie '2021-02', '2022-11-03' '2022-05-10-21:00'.i..).
+        A duration can be given with string format 'Xu' where X is a number and u is a unit in ('m':minutes, 'h':hours, 'd':days, 'M':months)
+        A datetime.datetime object or datetime.timedelta object will be used as is.
+        date2 can be None. In that case it is replaced by the current time.
+
+    method: str
+        Method of extraction
+            'nearest': Retrieve nearest value of date1, date2 is ignored.
+            'between': Retrive data between date1 and date2.
+
+    db: str
+        Which database to look in, 'H' or 'T'.
+
+    """
+
+    ## _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
+    #    Perform a few sanity checks
+    if not aea._check_initialized():
+        # Stop here, the function has produced a message if necessary
+        return
+
+    if not db in ("H", "T"):
+        raise ValueError("Attribute 'db' should be 'H' or 'T'")
+
+
+    allowedmethods=("nearest", "between", "minmaxmean")
+    if not method in allowedmethods:
+        raise ValueError("Attribute 'method' should be in {}".format(str(allowedmethods)))
+
+    ## _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
+    #     Work with dates
+    if not type(date1) in (datetime.datetime, datetime.timedelta):
+        date1 = aea._dateparse(date1)
+    if date2 is None:
+        date2 = datetime.datetime.now()
+    else:
+        if not type(date2) in (datetime.datetime, datetime.timedelta):
+            date2 = aea._dateparse(date2)
+
+    if not datetime.datetime in (type(date1), type(date2)):
+        logger.error("One of date1 date2 should be an exact date.\nGot {} {}".format(date1, date2))
+        raise ValueError("date1 and date2 not valid")
+
+    # Use timedelta relative to the other date. date1 is always before date2
+    if type(date1) is datetime.timedelta:
+        date1 = date2-date1
+    if type(date2) is datetime.timedelta:
+        date2 = date1+date2
+
+    if  date1 > date2:
+        logger.error("date1 must precede date2.\nGot {} {}".format(date1, date2))
+        raise ValueError("date1 and date2 not valid")
+
+    ## _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
+    #      Perform extraction and return
+
+    if type(attr) is dict:
+        d=dict()
+        for k,v in attr.items():
+            try:
+                d.update({k:aec._extract_attribute(v, method, date1, date2, db)})
+            except Exception as e:
+                logger.debug("Exception in _extract_attribute(): "+str(e))
+                logger.debug(traceback.print_tb(e.__traceback__))
+                logger.error("Could not extract {}.".format(v))
+
+        return d
+
+    if type(attr) in (list,tuple):
+        d=[]
+        for v in attr:
+            try:
+                d.append(aec._extract_attribute(v, method, date1, date2, db))
+            except Exception as e:
+                logger.debug("Exception in _extract_attribute(): "+str(e))
+                logger.debug(traceback.print_tb(e.__traceback__))
+                logger.error("Could not extract {}.".format(v))
+
+        return d
+
+    try:
+        d=aec._extract_attribute(attr, method, date1, date2, db)
+    except Exception as e:
+        logger.debug("Exception in _extract_attribute(): "+str(e))
+        logger.debug(traceback.print_tb(e.__traceback__))
+        logger.error("Could not extract {}.".format(attr))
+        return None
+
+    return d
+
+
+##----------------------------------------------------------------------##
+def findattr(pattern, db="H"):
+    """
+    Search for an attribute path using the pattern given.
+    Case insensitive.
+
+    Parameters:
+    -----------
+    pattern: str
+        Pattern to search, wildchar * accepted.
+        example "dg*dcct*current"
+
+    db: str
+        Which database to look in, 'H' or 'T'.
+
+    Returns:
+    --------
+    results: (str,)
+        List of string match
+    """
+    if not aea._check_initialized():
+        return
+
+    if not db in ("H", "T"):
+        raise AttributeError("Attribute db should be 'H' or 'T'")
+
+    global _AttrTables
+
+    keywords=pattern.lower().split('*')
+
+    # Select DB
+    attr_table = _AttrTables[{'H':0, 'T':1}[db]]
+
+    matches = [attr for attr in attr_table if all(k in attr.lower() for k in keywords)]
+
+    return matches
+
+##----------------------------------------------------------------------##
+def infoattr(attribute, db='H'):
+    """
+    Get informations for an attribute and pack it into a python dict.
+
+    Parameters
+    ----------
+    attribute : String
+        Name of the attribute. Full Tango name i.e. "test/dg/panda/current".
+
+    db: str
+        Which database to look in, 'H' or 'T'.
+
+    Returns
+    -------
+    info : dict
+        Dictionnary of propertyname:propertyvalue
+    """
+    if not aea._check_initialized():
+        return
+
+    if not db in ("H", "T"):
+        raise AttributeError("Attribute db should be 'H' or 'T'")
+
+    info = dict()
+
+    for func in ("GetAttDefinitionData", "GetAttPropertiesData"):
+        R=getattr(_extractors[{'H':0, 'T':1}[db]], func)(attribute)
+        if not R is None:
+            for i in R:
+                _s=i.split("::")
+                info[_s[0]]=_s[1]
+        else:
+            logger.warning("Function %s on extractor returned None"%func)
+
+    return info
+
+## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
+## Initialize on import
+## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
+init()
diff --git a/ArchiveExtractor/Amenities.py b/ArchiveExtractor/Amenities.py
new file mode 100644
index 0000000..5087ff8
--- /dev/null
+++ b/ArchiveExtractor/Amenities.py
@@ -0,0 +1,269 @@
+
+
+##########################################################################
+###               Commodity private variables                          ###
+##########################################################################
+
+# Extractor date format for GetAttDataBetweenDates
+_DBDFMT = "%Y-%m-%d %H:%M:%S"
+
+# Extractor date format for GetNearestValue
+_DBDFMT2 = "%d-%m-%Y %H:%M:%S"
+
+##########################################################################
+###               Commodity private functions                          ###
+##########################################################################
+
+# Vectorized fromtimestamp function
+# NOTE: it is faster than using pandas.to_datetime()
+_ArrayTimeStampToDatetime = np.vectorize(datetime.datetime.fromtimestamp)
+
+# Vectorized bool map dictionnary
+_ArrayStr2Bool = np.vectorize({
+    "true":True, 't':True,
+    "false":False, 'f':False,
+    }.get)
+
+
+def _check_initialized():
+    """
+    Check if the module is initialized.
+
+    Returns
+    -------
+    success : boolean
+    """
+    global _extractors
+    if None in _extractors:
+        logger.error("Module {0} is not initialied. You should run {0}.init().".format(__name__))
+        return False
+    return True
+
+##----------------------------------------------------------------------##
+def _dateparse(datestr):
+    """
+    Convenient function to parse date or duration strings.
+    Exact date format is %Y-%m-%d-%H:%M:%S and it can be reduced to be less precise.
+    Duration format is 'Xu' where X is a number and u is a unit in ('m':minutes, 'h':hours, 'd':days, 'M':months)
+    If datstr is None, take the actual date and time.
+
+    Parameters
+    ---------
+    datestr : string
+        Date as a string, format %Y-%m-%d-%H:%M:%S or less precise.
+        Duration as a string, format 'Xu' where X is a number and u is a unit in ('m':minutes, 'h':hours, 'd':days, 'M':months)
+
+    Exceptions
+    ----------
+    ValueError
+        If the parsing failed.
+
+    Returns
+    -------
+    date : datetime.datetime or datetime.timedelta
+        Parsed date or duration
+    """
+    logger.debug("Parsing date string '%s'"%datestr)
+
+    # Determine date/duration by looking at the last char
+    if datestr[-1] in "mhdM":
+        # Duration
+        logger.debug("Assuming a duration")
+
+        try:
+            q=float(datestr[:-1])
+        except ValueError as e:
+            logger.error("Failed to parse date string. Given the last character, a duration was assumed.")
+            raise Exception("Could not parse argument to a date") from e
+
+        # Convert all in minutes
+        minutes = q*{'m':1, 'h':60, 'd':60*24, 'm':30*60*24}[datestr[-1]]
+
+        return datetime.timedelta(minutes=minutes)
+
+    else:
+        # Probably a date string
+
+        # This gives all format that will be tried, in order.
+        # Stop on first parse success. Raise error if none succeed.
+        fmt = [
+            "%Y-%m-%d-%H:%M:%S",
+            "%Y-%m-%d-%H:%M",
+            "%Y-%m-%d-%H",
+            "%Y-%m-%d",
+            "%Y-%m",
+            ]
+
+        date = None
+        for f in fmt:
+            try:
+                date = datetime.datetime.strptime(datestr, f)
+            except ValueError:
+                continue
+            else:
+                break
+        else:
+            raise ValueError("Could not parse argument to a date")
+
+        return date
+
+##----------------------------------------------------------------------##
+def _check_attribute(attribute, db):
+    """
+    Check that the attribute is in the database
+
+    Parameters
+    ----------
+    attribute : String
+        Name of the attribute. Full Tango name i.e. "test/dg/panda/current".
+
+    db: str
+        Which database to look in, 'H' or 'T'.
+    """
+    global _extractors
+
+    logger.debug("Check that %s is archived."%attribute)
+    if not _extractors[{'H':0, 'T':1}[db]].IsArchived(attribute):
+        logger.error("Attribute '%s' is not archived in DB %s"%(attribute, _extractors[{'H':0, 'T':1}[db]]))
+        raise ValueError("Attribute '%s' is not archived in DB %s"%(attribute, _extractors[{'H':0, 'T':1}[db]]))
+
+##----------------------------------------------------------------------##
+def _chunkerize(attribute, dateStart, dateStop, db, Nmax=100000):
+    """
+
+    Parameters
+    ----------
+    attribute : String
+        Name of the attribute. Full Tango name i.e. "test/dg/panda/current".
+
+    dateStart : datetime.datetime
+        Start date for extraction.
+
+    dateStop : datetime.datetime
+        Stop date for extraction.
+
+    db: str
+        Which database to look in, 'H' or 'T'.
+
+    Nmax: int
+        Max number of atoms in one chunk. Default 100000.
+
+    Returns
+    -------
+    cdates : list
+        List of datetime giving the limit of each chunks.
+        For N chunks, there is N+1 elements in cdates, as the start and end boundaries are included.
+    """
+    info=infoattr(attribute, db=db)
+    logger.debug("Attribute information \n%s"%info)
+
+    # Get the number of points
+    N=_extractors[{'H':0, 'T':1}[db]].GetAttDataBetweenDatesCount([
+            attribute,
+            dateStart.strftime(_DBDFMT2),
+            dateStop.strftime(_DBDFMT2)
+            ])
+    logger.debug("On the period, there is %d entries"%N)
+
+    dx=int(info["max_dim_x"])
+    if dx > 1:
+        logger.debug("Attribute is a vector with max dimension = %s"%dx)
+        N=N*dx
+
+    # If data chunk is too much, we need to cut it
+    if N > Nmax:
+        dt = (dateStop-dateStart)/(N//Nmax)
+        cdates = [dateStart]
+        while cdates[-1] < dateStop:
+            cdates.append(cdates[-1]+dt)
+        cdates[-1] = dateStop
+        logger.debug("Cutting access to %d little chunks of time, %s each."%(len(cdates)-1, dt))
+    else:
+        cdates=[dateStart, dateStop]
+
+    return cdates
+
+##----------------------------------------------------------------------##
+def _cmd_with_retry(dp, cmd, arg, retry=2):
+    """
+    Run a command on tango.DeviceProxy, retrying on DevFailed.
+
+    Parameters
+    ----------
+    dp: tango.DeviceProxy
+        Device proxy to try command onto.
+
+    cmd : str
+        Command to executte on the extractor
+
+    arg : list
+        Attribute to pass to the command
+
+    retry : int
+        Number of command retry on DevFailed
+
+    Returns
+    -------
+    cmdreturn :
+        Whatever the command returns.
+        None if failed after the amount of retries.
+    """
+    logger.info("Perform Command {} {}".format(cmd, arg))
+
+    for i in range(retry):
+        # Make retrieval request
+        logger.debug("Execute %s (%s)"%(cmd, arg))
+        try:
+            cmdreturn = getattr(dp, cmd)(arg)
+        except tango.DevFailed as e:
+            logger.warning("The extractor device returned the following error:")
+            logger.warning(e)
+            if  i == retry-1:
+                logger.error("Could not execute command %s (%s). Check the device extractor"%(cmd, arg))
+                return None
+            logger.warning("Retrying...")
+            continue
+        break
+    return cmdreturn
+
+
+def _cast_bool(value):
+    """
+    Cast a value, or array of values, to boolean.
+    Try to assess the input data type. If string, then try to find true or false word inside.
+
+    Parameters:
+    -----------
+    value: string, integer, or array of such
+        value to convert.
+
+    Return:
+    boolean:
+        value or array of boolean.
+    """
+
+    # Force to array
+    value = np.asarray(value)
+
+    # cast back to single value
+    def castback(v):
+        if v.shape == ():
+            return v.item()
+        return v
+
+    # Simply try to cast to bool first
+    try:
+        value = value.astype("bool")
+        logger.debug("Direct conversion to boolean")
+        return castback(value)
+    except ValueError:
+        # Keep trying to cast
+        pass
+
+    logger.debug("Try to convert to boolean")
+
+    value = np.char.strip(np.char.lower(value))
+    value = _ArrayStr2Bool(value)
+
+    return castback(value)
+
diff --git a/ArchiveExtractor/Core.py b/ArchiveExtractor/Core.py
new file mode 100644
index 0000000..e35d542
--- /dev/null
+++ b/ArchiveExtractor/Core.py
@@ -0,0 +1,314 @@
+
+
+##########################################################################
+###                    Module core functions                           ###
+##########################################################################
+
+def _extract_attribute(attribute, method, date1, date2, db):
+    """
+    Check if exists, check scalar or spectrum and dispatch
+    """
+
+    # Uncapitalize attribute
+    attribute = attribute.lower()
+    aea._check_attribute(attribute, db)
+
+    # Get info about the attribute
+    info=infoattr(attribute, db=db)
+    logger.debug("Attribute information \n%s"%info)
+
+    # Detect spectrum
+    attrtype="scalar"
+    if int(info["max_dim_x"]) > 1:
+        if int(info["max_dim_y"]) > 0:
+            logger.warning("Attribute %s is a (%s; %s) vector. This is poorly handled by this module."%(
+                attribute, info["max_dim_x"], info["max_dim_y"]))
+            attrtype="multi"
+        else:
+            logger.info("Attribute %s is a 1D vector, dimension = %s."%(
+                attribute, info["max_dim_x"]))
+            attrtype="vector"
+
+    # =============
+    # For now we handle multi dimension the same way as scalar, which will get only the first element
+    if (attrtype=="scalar") or (attrtype=="multi"):
+        if info["data_type"] == '1':
+            # Boolean data type, quick fix
+            dtype=bool
+        else:
+            dtype=float
+
+        return _extract_scalar(attribute, method, date1, date2, db, dtype)
+    if attrtype=="vector":
+        return _extract_vector(attribute, method, date1, date2, db)
+
+
+##---------------------------------------------------------------------------##
+def _extract_scalar(attribute, method, date1, date2, db, dtype):
+
+    # =====================
+    if method == "nearest":
+        cmdreturn = aea._cmd_with_retry(_extractors[{'H':0, 'T':1}[db]], "GetNearestValue", [
+                                                attribute,
+                                                date1.strftime(aea._DBDFMT),
+                                                ])
+
+        # Unpack return
+        try:
+            _date, _value = cmdreturn.split(';')
+        except TypeError:
+            logger.error("Could not extract this chunk. Check the device extractor")
+            return None
+
+        # Transform by datatype
+        if dtype is bool:
+            _value = _cast_bool(_value)
+
+        # Fabricate return pandas.Series
+        d=pd.Series(index=[datetime.datetime.fromtimestamp(int(_date)/1000),], data=[_value,], name=attribute)
+
+        return d
+
+    # =====================
+    if method == "between":
+        # Cut the time horizon in chunks
+        cdates = aea._chunkerize(attribute, date1, date2, db)
+
+        # Array to hold data
+        data = []
+
+        # For each date chunk
+        for i_d in range(len(cdates)-1):
+            cmdreturn = aea._cmd_with_retry(_extractors[{'H':0, 'T':1}[db]], "ExtractBetweenDates", [
+                                                    attribute,
+                                                    cdates[i_d].strftime(aea._DBDFMT),
+                                                    cdates[i_d+1].strftime(aea._DBDFMT)
+                                                    ])
+
+
+            # Unpack return
+            try:
+                _date, _value = cmdreturn
+            except TypeError:
+                logger.error("Could not extract this chunk. Check the device extractor")
+                return None
+
+
+            # Transform to datetime - value arrays
+            if dtype is bool:
+                _value = _cast_bool(_value)
+            else:
+                _value = np.asarray(_value, dtype=dtype)
+
+            if len(_date) > 0:
+                _date = aea._ArrayTimeStampToDatetime(_date/1000.0)
+
+            # Fabricate return pandas.Series
+            data.append(pd.Series(index=_date, data=_value, name=attribute))
+
+        # Concatenate chunks
+        return pd.concat(data)
+
+    # ========================
+    if method == "minmaxmean":
+
+        # If we are here, the method is not implemented
+        logger.error("Method {} is not implemented for scalars.".format(method))
+        raise NotImplemented
+
+##---------------------------------------------------------------------------##
+def _extract_vector(attribute, method, date1, date2, db):
+
+    # Get info about the attribute
+    info=infoattr(attribute, db=db)
+
+    # =====================
+    if method == "nearest":
+        # Get nearest does not work with vector.
+        # Make a between date with surounding dates.
+
+        # Dynamically find surounding
+        cnt=0
+        dt=datetime.timedelta(seconds=10)
+        while cnt<1:
+            logger.debug("Seeking points in {} to {}".format(date1-dt,date1+dt))
+            cnt=_extractors[{'H':0, 'T':1}[db]].GetAttDataBetweenDatesCount([
+                    attribute,
+                    (date1-dt).strftime(aea._DBDFMT2),
+                    (date1+dt).strftime(aea._DBDFMT2)
+                    ])
+            dt=dt*1.5
+        logger.debug("Found {} points in a +- {} interval".format(cnt,str(dt/1.5)))
+
+
+        # For vector, we have to use the GetAttxxx commands
+        cmdreturn = aea._cmd_with_retry(_extractors[{'H':0, 'T':1}[db]], "GetAttDataBetweenDates", [
+                                                attribute,
+                                                (date1-dt).strftime(aea._DBDFMT),
+                                                (date1+dt).strftime(aea._DBDFMT),
+                                                ])
+
+        # Unpack return
+        try:
+            [N,], [name,] = cmdreturn
+            N=int(N)
+        except TypeError:
+            logger.error("Could not extract this attribute. Check the device extractor")
+            return None
+
+        # Read the history
+        logger.debug("Retrieve history of %d values. Dynamic attribute named %s."%(N, name))
+        attrHist = _extractors[{'H':0, 'T':1}[db]].attribute_history(name, N)
+
+        # Transform to datetime - value arrays
+        _value = np.empty((N, int(info["max_dim_x"])), dtype=float)
+        _value[:] = np.nan
+        _date = np.empty(N, dtype=object)
+        for i_h in range(N):
+            _value[i_h,:attrHist[i_h].dim_x]=attrHist[i_h].value
+            _date[i_h]=attrHist[i_h].time.todatetime()
+
+        # Seeking nearest entry
+        idx=np.argmin(abs(_date-date1))
+        logger.debug("Found nearest value at index {}: {}".format(idx, _date[idx]))
+
+        # Fabricate return pandas.Series
+        d=pd.Series(index=[_date[idx],], data=[_value[idx],], name=attribute)
+
+        return d
+
+    # If we are here, the method is not implemented
+    logger.error("Method {} is not implemented for vectors.".format(method))
+    raise NotImplemented
+
+
+##---------------------------------------------------------------------------##
+def ExtrBetweenDates_MinMaxMean(
+        attribute,
+        dateStart,
+        dateStop=None,
+        timeInterval=datetime.timedelta(seconds=60),
+        db='H',
+        ):
+    """
+    Query attribute data from an archiver database, get all points between dates.
+    Use ExtractBetweenDates.
+
+    Parameters
+    ----------
+    attribute : String
+        Name of the attribute. Full Tango name i.e. "test/dg/panda/current".
+
+    dateStart : datetime.datetime, string
+        Start date for extraction. If string, it will be parsed.
+        Example of string format %Y-%m-%d-%H:%M:%S or less precise.
+
+    dateStop : datetime.datetime, string
+        Stop date for extraction. If string, it will be parsed.
+        Example of string format %Y-%m-%d-%H:%M:%S or less precise.
+        Default is now (datetime.datetime.now())
+
+    timeInterval: datetime.timedelta, string
+        Time interval used to perform min,max and mean.
+        Can be a string with a number and a unit in "d", "h", "m" or "s"
+
+    db: str
+        Which database to look in, 'H' or 'T'.
+
+    Exceptions
+    ----------
+    ValueError
+        The attribute is not found in the database.
+
+    Returns
+    -------
+    [mdates, value_min, value_max, value_mean] : array
+        mdates : numpy.ndarray of datetime.datime objects
+            Dates of the values, middle of timeInterval windows
+        value_min : numpy.ndarray
+            Minimum of the value on the interval
+        value_max : numpy.ndarray
+            Maximum of the value on the interval
+        value_mean : numpy.ndarray
+            Mean of the value on the interval
+
+    """
+    if not _check_initialized():
+        return
+
+    if not db in ("H", "T"):
+        raise AttributeError("Attribute db should be 'H' or 'T'")
+
+    # Uncapitalize attribute
+    attribute = attribute.lower()
+
+    # Check attribute is in database
+    _check_attribute(attribute, db=db)
+
+    # Parse dates
+    dateStart = _dateparse(dateStart)
+    dateStop = _dateparse(dateStop)
+
+    # Parse timeInterval if string
+    if type(timeInterval) is str:
+        try:
+            mul = {'s':1, 'm':60, 'h':60*60, 'd':60*60*24}[timeInterval[-1]]
+        except KeyError:
+            logger.error("timeInterval could not be parsed")
+            raise ValueError("timeInterval could not be parsed")
+        timeInterval= datetime.timedelta(seconds=int(timeInterval[:-1])*mul)
+
+    # Get info about the attribute
+    info=infoattr(attribute)
+    logger.debug("Attribute information \n%s"%info)
+
+    # Detect spectrum
+    attrtype="scalar"
+    if int(info["max_dim_x"]) > 1:
+        logger.error("Attribute is not a scalar. Cannot perform this kind of operation.")
+        return None
+
+    # Cut data range in time chunks
+    cdates = [dateStart]
+    while cdates[-1] < dateStop:
+        cdates.append(cdates[-1]+timeInterval)
+    cdates[-1] = dateStop
+    mdates = np.asarray(cdates[:-1])+timeInterval/2
+    logger.debug("Cutting time range to %d chunks of time, %s each."%(len(cdates)-1, timeInterval))
+
+    # Prepare arrays
+    value_min = np.empty(len(cdates)-1)
+    value_max = np.empty(len(cdates)-1)
+    value_mean = np.empty(len(cdates)-1)
+
+    # For each time chunk
+    for i_d in range(len(cdates)-1):
+        for func, arr in zip(
+                ["Max", "Min", "Avg"],
+                [value_max, value_min, value_mean],
+                ):
+            # Make requests
+            logger.debug("Perform GetAttData%sBetweenDates (%s, %s, %s)"%(
+                func,
+                attribute,
+                cdates[i_d].strftime(_DBDFMT2),
+                cdates[i_d+1].strftime(_DBDFMT2))
+                )
+
+            _val =getattr(_extractors[{'H':0, 'T':1}[db]], "GetAttData%sBetweenDates"%func)([
+                attribute,
+                cdates[i_d].strftime(_DBDFMT2),
+                cdates[i_d+1].strftime(_DBDFMT2)
+                ])
+
+            arr[i_d] = _val
+
+    logger.debug("Extraction done for %s."%attribute)
+    return pd.DataFrame(
+            index=mdates,
+            data={
+                "Min":value_min,
+                "Mean":value_mean,
+                "Max":value_max,
+                },)
+
diff --git a/ArchiveExtractor/__init__.py b/ArchiveExtractor/__init__.py
new file mode 100644
index 0000000..a8ea30b
--- /dev/null
+++ b/ArchiveExtractor/__init__.py
@@ -0,0 +1,7 @@
+"""
+Python module for extracting attribute from Archive Extractor Device.
+"""
+
+__version__ = "AUTOVERSIONREPLACE"
+
+__all__ = ["Access", ]
diff --git a/__init__.py b/__init__.py
deleted file mode 100644
index e69de29..0000000
-- 
GitLab