Mercurial > personal > weather-server
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 20:a7fe635d1c88 | 21:beb42c835c52 |
|---|---|
| 1 """Manages the directory containing the various logs.""" | 1 """Manages the directory containing the various logs.""" |
| 2 | 2 |
| 3 import configparser | 3 import configparser |
| 4 import datetime | |
| 4 import pathlib | 5 import pathlib |
| 5 import typing as t | 6 import typing as t |
| 6 | 7 |
| 7 import attr | |
| 8 import pytz | 8 import pytz |
| 9 | 9 |
| 10 from . import logfile | 10 from . import logfile |
| 11 from . import types | 11 from . import types |
| 12 | 12 |
| 13 | |
| 14 CONFIG_FILE = 'config.ini' | 13 CONFIG_FILE = 'config.ini' |
| 15 LOG = 'log.bson' | 14 LOG = 'log.bson' |
| 16 | 15 |
| 17 | 16 |
| 18 @attr.s(frozen=True, slots=True) | 17 class ConfigError(Exception): |
| 19 class LocationInfo: | 18 """Raised when a location can't be loaded.""" |
| 20 name = attr.ib(type=str) | |
| 21 tz_name = attr.ib(type=str) | |
| 22 password = attr.ib(type=str) | |
| 23 | 19 |
| 24 def timezone(self): | 20 |
| 21 class Location: | |
| 22 | |
| 23 def __init__(self, root: pathlib.Path): | |
| 24 parser = configparser.ConfigParser(interpolation=None) | |
| 25 self.root = root | |
| 26 config_file = root / CONFIG_FILE | |
| 27 try: | |
| 28 with open(config_file, 'r') as infile: | |
| 29 parser.read_file(infile) | |
| 30 self.location = parser.get( | |
| 31 'location', 'name', fallback='Weather station') | |
| 32 self.tz_name = parser.get('location', 'timezone', fallback='UTC') | |
| 33 self.password = parser.get('location', 'password') | |
| 34 self.logger = logfile.Logger.create( | |
| 35 str(root / LOG), sample_field='sample_time') | |
| 36 except (IOError, KeyError, configparser.Error): | |
| 37 raise ConfigError("Couldn't load location info.") | |
| 38 | |
| 39 def record( | |
| 40 self, | |
| 41 entries: t.Iterable[t.Dict[str, object]], | |
| 42 timestamp: datetime.datetime, | |
| 43 ) -> None: | |
| 44 for e in entries: | |
| 45 e['ingest_time'] = timestamp | |
| 46 self.logger.write_rows(entries) | |
| 47 | |
| 48 @property | |
| 49 def entries(self) -> t.Iterable[t.Dict[str, object]]: | |
| 50 return self.logger.data | |
| 51 | |
| 52 def latest(self) -> t.Optional[types.Reading]: | |
| 53 most_recent = reversed(self.logger.data) | |
| 54 for entry in most_recent: | |
| 55 try: | |
| 56 return types.Reading.from_dict(entry) | |
| 57 except KeyError: | |
| 58 pass # go to the older one. | |
| 59 return None | |
| 60 | |
| 61 def timezone(self) -> datetime.tzinfo: | |
| 25 try: | 62 try: |
| 26 return pytz.timezone(self.tz_name) | 63 return pytz.timezone(self.tz_name) |
| 27 except pytz.UnknownTimeZoneError: | 64 except pytz.UnknownTimeZoneError: |
| 28 return pytz.UTC | 65 return pytz.UTC |
| 29 | 66 |
| 30 @classmethod | 67 def __repr__(self) -> str: |
| 31 def load(cls, config_file: pathlib.Path) -> 'LocationInfo': | 68 return '<Location in %r>'.format(self.root) |
| 32 parser = configparser.ConfigParser(interpolation=None) | |
| 33 parser.read(config_file) | |
| 34 return LocationInfo( | |
| 35 name=parser.get('location', 'name', fallback='Weather station'), | |
| 36 tz_name=parser.get('location', 'timezone', fallback='UTC'), | |
| 37 password=parser.get('location', 'password')) | |
| 38 | 69 |
| 39 | 70 |
| 40 class Locations: | 71 class LocationFolder: |
| 41 def __init__(self, base: str): | |
| 42 self._path = pathlib.Path(base) | |
| 43 | 72 |
| 44 def paths(self) -> t.Tuple[str, ...]: | 73 def __init__(self, root: pathlib.Path): |
| 45 return tuple(sorted(f.name for f in self._path.iterdir())) | 74 self.root = root |
| 75 # locations, mtime | |
| 76 self.info: t.Tuple[t.Dict[str, Location], t.Optional[int]] = ({}, None) | |
| 77 self._maybe_reload() | |
| 46 | 78 |
| 47 def get(self, name) -> t.Tuple[LocationInfo, logfile.Logger]: | 79 def get(self, name: str) -> Location: |
| 48 try: | 80 self._maybe_reload() |
| 49 directory = self._path / name | 81 locs, _ = self.info |
| 50 logger = logfile.Logger.create(str(directory / LOG)) | 82 return locs[name] |
| 51 return (LocationInfo.load(directory / CONFIG_FILE), logger) | 83 |
| 52 except OSError: | 84 def _maybe_reload(self) -> None: |
| 53 raise KeyError(name) | 85 new_mtime = self.root.stat().st_mtime_ns |
| 86 _, old_mtime = self.info | |
| 87 if old_mtime == new_mtime: | |
| 88 return | |
| 89 locations = {} | |
| 90 for child in self.root.iterdir(): | |
| 91 try: | |
| 92 locations[child.name] = Location(child) | |
| 93 except ConfigError: | |
| 94 pass # It's OK. Skip this. | |
| 95 self.info = locations, new_mtime |
