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 |