view weather_server/locations.py @ 21:beb42c835c52

Make weather server handle arbitrary data: - Make logfile record arbitrary BSONs - Make server handlers OK with same - Make location type a normal class rather than attrs; have it handle its own logger. - Bump version number.
author Paul Fisher <paul@pfish.zone>
date Sat, 19 Oct 2019 18:40:48 -0400
parents f1ea183d28ba
children e229afdd447b
line wrap: on
line source

"""Manages the directory containing the various logs."""

import configparser
import datetime
import pathlib
import typing as t

import pytz

from . import logfile
from . import types

CONFIG_FILE = 'config.ini'
LOG = 'log.bson'


class ConfigError(Exception):
    """Raised when a location can't be loaded."""


class Location:

    def __init__(self, root: pathlib.Path):
        parser = configparser.ConfigParser(interpolation=None)
        self.root = root
        config_file = root / CONFIG_FILE
        try:
            with open(config_file, 'r') as infile:
                parser.read_file(infile)
            self.location = parser.get(
                'location', 'name', fallback='Weather station')
            self.tz_name = parser.get('location', 'timezone', fallback='UTC')
            self.password = parser.get('location', 'password')
            self.logger = logfile.Logger.create(
                str(root / LOG), sample_field='sample_time')
        except (IOError, KeyError, configparser.Error):
            raise ConfigError("Couldn't load location info.")

    def record(
        self,
        entries: t.Iterable[t.Dict[str, object]],
        timestamp: datetime.datetime,
    ) -> None:
        for e in entries:
            e['ingest_time'] = timestamp
        self.logger.write_rows(entries)

    @property
    def entries(self) -> t.Iterable[t.Dict[str, object]]:
        return self.logger.data

    def latest(self) -> t.Optional[types.Reading]:
        most_recent = reversed(self.logger.data)
        for entry in most_recent:
            try:
                return types.Reading.from_dict(entry)
            except KeyError:
                pass  # go to the older one.
        return None

    def timezone(self) -> datetime.tzinfo:
        try:
            return pytz.timezone(self.tz_name)
        except pytz.UnknownTimeZoneError:
            return pytz.UTC

    def __repr__(self) -> str:
        return '<Location in %r>'.format(self.root)


class LocationFolder:

    def __init__(self, root: pathlib.Path):
        self.root = root
        # locations, mtime
        self.info: t.Tuple[t.Dict[str, Location], t.Optional[int]] = ({}, None)
        self._maybe_reload()

    def get(self, name: str) -> Location:
        self._maybe_reload()
        locs, _ = self.info
        return locs[name]

    def _maybe_reload(self) -> None:
        new_mtime = self.root.stat().st_mtime_ns
        _, old_mtime = self.info
        if old_mtime == new_mtime:
            return
        locations = {}
        for child in self.root.iterdir():
            try:
                locations[child.name] = Location(child)
            except ConfigError:
                pass  # It's OK. Skip this.
        self.info = locations, new_mtime