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