summaryrefslogtreecommitdiff
path: root/src/home
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2022-10-25 02:41:46 +0300
committerEvgeny Zinoviev <me@ch1p.io>2022-10-30 01:37:35 +0300
commitdb784dc98ba0c4e15ee7b501d909425c79c825fb (patch)
tree68270179f77c95dc49ec4ab62f9305aa651c4cd4 /src/home
parent0fce6c52516aba239acc81fd528dcb5051c04f68 (diff)
web_api: rewrite to aiohttp, drop flask
Diffstat (limited to 'src/home')
-rw-r--r--src/home/api/web_api_client.py6
-rw-r--r--src/home/config/config.py2
-rw-r--r--src/home/database/clickhouse.py29
-rw-r--r--src/home/database/inverter.py31
-rw-r--r--src/home/database/sensors.py8
-rw-r--r--src/home/http/__init__.py2
-rw-r--r--src/home/web_api/__init__.py1
-rw-r--r--src/home/web_api/web_api.py213
8 files changed, 42 insertions, 250 deletions
diff --git a/src/home/api/web_api_client.py b/src/home/api/web_api_client.py
index d6c9dc7..299bb6e 100644
--- a/src/home/api/web_api_client.py
+++ b/src/home/api/web_api_client.py
@@ -59,7 +59,7 @@ class WebAPIClient:
bot: BotType,
user_id: int,
message: str):
- return self._post('logs/bot-request/', {
+ return self._post('log/bot-request/', {
'bot': bot.value,
'user_id': str(user_id),
'message': message
@@ -67,7 +67,7 @@ class WebAPIClient:
def log_openwrt(self,
lines: List[Tuple[int, str]]):
- return self._post('logs/openwrt', {
+ return self._post('log/openwrt/', {
'logs': stringify(lines)
})
@@ -159,7 +159,7 @@ class WebAPIClient:
kwargs['files'] = fd
try:
- r = f(f'https://{domain}/api/{name}',
+ r = f(f'https://{domain}/{name}',
headers={'X-Token': self.token},
timeout=self.timeout,
**kwargs)
diff --git a/src/home/config/config.py b/src/home/config/config.py
index 9882bfa..7d18f99 100644
--- a/src/home/config/config.py
+++ b/src/home/config/config.py
@@ -113,7 +113,7 @@ config = ConfigStore()
def is_development_mode() -> bool:
- if 'FLASK_ENV' in os.environ and os.environ['FLASK_ENV'] == 'development':
+ if 'HK_MODE' in os.environ and os.environ['HK_MODE'] == 'dev':
return True
return ('logging' in config) and ('verbose' in config['logging']) and (config['logging']['verbose'] is True)
diff --git a/src/home/database/clickhouse.py b/src/home/database/clickhouse.py
index 4a2a247..ca81628 100644
--- a/src/home/database/clickhouse.py
+++ b/src/home/database/clickhouse.py
@@ -1,4 +1,9 @@
+import logging
+
+from zoneinfo import ZoneInfo
+from datetime import datetime, timedelta
from clickhouse_driver import Client as ClickhouseClient
+from ..config import is_development_mode
_links = {}
@@ -8,3 +13,27 @@ def get_clickhouse(db: str) -> ClickhouseClient:
_links[db] = ClickhouseClient.from_url(f'clickhouse://localhost/{db}')
return _links[db]
+
+
+class ClickhouseDatabase:
+ def __init__(self, db: str):
+ self.db = get_clickhouse(db)
+
+ self.server_timezone = self.db.execute('SELECT timezone()')[0][0]
+ self.logger = logging.getLogger(self.__class__.__name__)
+
+ def query(self, *args, **kwargs):
+ settings = {'use_client_time_zone': True}
+ kwargs['settings'] = settings
+
+ if 'no_tz_fix' not in kwargs and len(args) > 1 and isinstance(args[1], dict):
+ for k, v in args[1].items():
+ if isinstance(v, datetime):
+ args[1][k] = v.astimezone(tz=ZoneInfo(self.server_timezone))
+
+ result = self.db.execute(*args, **kwargs)
+
+ if is_development_mode():
+ self.logger.debug(args[0] if len(args) == 1 else args[0] % args[1])
+
+ return result
diff --git a/src/home/database/inverter.py b/src/home/database/inverter.py
index 756186c..1e967c4 100644
--- a/src/home/database/inverter.py
+++ b/src/home/database/inverter.py
@@ -1,40 +1,17 @@
-import logging
-
-from zoneinfo import ZoneInfo
from time import time
-from datetime import datetime, timedelta
+from datetime import datetime
from typing import Optional
from collections import namedtuple
-from ..config import is_development_mode
-from .clickhouse import get_clickhouse
+from .clickhouse import ClickhouseDatabase
IntervalList = list[list[Optional[datetime]]]
-class InverterDatabase:
+class InverterDatabase(ClickhouseDatabase):
def __init__(self):
- self.db = get_clickhouse('solarmon')
- self.server_timezone = self.query('SELECT timezone()')[0][0]
-
- self.logger = logging.getLogger(self.__class__.__name__)
-
- def query(self, *args, **kwargs):
- settings = {'use_client_time_zone': True}
- kwargs['settings'] = settings
-
- if 'no_tz_fix' not in kwargs and len(args) > 1 and isinstance(args[1], dict):
- for k, v in args[1].items():
- if isinstance(v, datetime):
- args[1][k] = v.astimezone(tz=ZoneInfo(self.server_timezone))
-
- result = self.db.execute(*args, **kwargs)
-
- if is_development_mode():
- self.logger.debug(args[0] if len(args) == 1 else args[0] % args[1])
-
- return result
+ super().__init__('solarmon')
def add_generation(self, home_id: int, client_time: int, watts: int) -> None:
self.db.execute(
diff --git a/src/home/database/sensors.py b/src/home/database/sensors.py
index 4cfaa08..8155108 100644
--- a/src/home/database/sensors.py
+++ b/src/home/database/sensors.py
@@ -1,7 +1,7 @@
from time import time
from datetime import datetime
from typing import Tuple, List
-from .clickhouse import get_clickhouse
+from .clickhouse import ClickhouseDatabase
from ..api.types import TemperatureSensorLocation
@@ -25,9 +25,9 @@ def get_temperature_table(sensor: TemperatureSensorLocation) -> str:
return 'temp_spb1'
-class SensorsDatabase:
+class SensorsDatabase(ClickhouseDatabase):
def __init__(self):
- self.db = get_clickhouse('home')
+ super().__init__('home')
def add_temperature(self,
home_id: int,
@@ -62,7 +62,7 @@ class SensorsDatabase:
ORDER BY ClientTime"""
dt_from, dt_to = time_range
- data = self.db.execute(sql, {
+ data = self.query(sql, {
'from': dt_from,
'to': dt_to
})
diff --git a/src/home/http/__init__.py b/src/home/http/__init__.py
index 963e13c..6030e95 100644
--- a/src/home/http/__init__.py
+++ b/src/home/http/__init__.py
@@ -1,2 +1,2 @@
from .http import serve, ok, routes, HTTPServer
-from aiohttp.web import FileResponse, StreamResponse, Request \ No newline at end of file
+from aiohttp.web import FileResponse, StreamResponse, Request, Response
diff --git a/src/home/web_api/__init__.py b/src/home/web_api/__init__.py
deleted file mode 100644
index 20655da..0000000
--- a/src/home/web_api/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from .web_api import get_app \ No newline at end of file
diff --git a/src/home/web_api/web_api.py b/src/home/web_api/web_api.py
deleted file mode 100644
index 6b8c54e..0000000
--- a/src/home/web_api/web_api.py
+++ /dev/null
@@ -1,213 +0,0 @@
-import logging
-import json
-import os.path
-
-from datetime import datetime, timedelta
-from typing import Optional
-
-from werkzeug.exceptions import HTTPException
-from flask import Flask, request, Response
-
-from ..config import config, is_development_mode
-from ..database import BotsDatabase, SensorsDatabase
-from ..util import stringify, format_tb
-from ..api.types import BotType, TemperatureSensorLocation, SoundSensorLocation
-from ..media import SoundRecordStorage
-
-db: Optional[BotsDatabase] = None
-sensors_db: Optional[SensorsDatabase] = None
-app = Flask(__name__)
-logger = logging.getLogger(__name__)
-
-
-class AuthError(Exception):
- def __init__(self, message: str):
- super().__init__()
- self.message = message
-
-
-# api methods
-# -----------
-
-@app.route("/")
-def hello():
- message = "nothing here, keep lurking"
- if is_development_mode():
- message += ' (dev mode)'
- return message
-
-
-@app.route('/api/sensors/data/', methods=['GET'])
-def sensors_data():
- hours = request.args.get('hours', type=int, default=1)
- sensor = TemperatureSensorLocation(request.args.get('sensor', type=int))
-
- if hours < 1 or hours > 24:
- raise ValueError('invalid hours value')
-
- dt_to = datetime.now()
- dt_from = dt_to - timedelta(hours=hours)
-
- data = sensors_db.get_temperature_recordings(sensor, (dt_from, dt_to))
- return ok(data)
-
-
-@app.route('/api/sound_sensors/hits/', methods=['GET'])
-def get_sound_sensors_hits():
- location = SoundSensorLocation(request.args.get('location', type=int))
-
- after = request.args.get('after', type=int)
- kwargs = {}
- if after is None:
- last = request.args.get('last', type=int)
- if last is None:
- raise ValueError('you must pass `after` or `last` params')
- else:
- if not 0 < last < 100:
- raise ValueError('invalid last value: must be between 0 and 100')
- kwargs['last'] = last
- else:
- kwargs['after'] = datetime.fromtimestamp(after)
-
- data = db.get_sound_hits(location, **kwargs)
- return ok(data)
-
-
-@app.route('/api/sound_sensors/hits/', methods=['POST'])
-def post_sound_sensors_hits():
- hits = []
- for hit, count in json.loads(request.form.get('hits', type=str)):
- if not hasattr(SoundSensorLocation, hit.upper()):
- raise ValueError('invalid sensor location')
- if count < 1:
- raise ValueError(f'invalid count: {count}')
- hits.append((SoundSensorLocation[hit.upper()], count))
-
- db.add_sound_hits(hits, datetime.now())
- return ok()
-
-
-@app.route('/api/logs/bot-request/', methods=['POST'])
-def log_bot_request():
- user_id = request.form.get('user_id', type=int, default=0)
- message = request.form.get('message', type=str, default='')
- bot = BotType(request.form.get('bot', type=int))
-
- # validate message
- if message.strip() == '':
- raise ValueError('message can\'t be empty')
-
- # add record to the database
- db.add_request(bot, user_id, message)
-
- return ok()
-
-
-@app.route('/api/logs/openwrt/', methods=['POST'])
-def log_openwrt():
- logs = request.form.get('logs', type=str, default='')
-
- # validate it
- logs = json.loads(logs)
- assert type(logs) is list, "invalid json data (list expected)"
-
- lines = []
- for line in logs:
- assert type(line) is list, "invalid line type (list expected)"
- assert len(line) == 2, f"expected 2 items in line, got {len(line)}"
- assert type(line[0]) is int, "invalid line[0] type (int expected)"
- assert type(line[1]) is str, "invalid line[1] type (str expected)"
-
- lines.append((
- datetime.fromtimestamp(line[0]),
- line[1]
- ))
-
- db.add_openwrt_logs(lines)
- return ok()
-
-
-@app.route('/api/recordings/list/', methods=['GET'])
-def recordings_list():
- extended = request.args.get('extended', type=bool, default=False)
- node = request.args.get('node', type=str)
-
- root = os.path.join(config['recordings']['directory'], node)
- if not os.path.isdir(root):
- raise ValueError(f'invalid node {node}: no such directory')
-
- storage = SoundRecordStorage(root)
- files = storage.getfiles(as_objects=extended)
- if extended:
- files = list(map(lambda file: file.__dict__(), files))
-
- return ok(files)
-
-
-# internal functions
-# ------------------
-
-def ok(data=None) -> Response:
- response = {'result': 'ok'}
- if data is not None:
- response['data'] = data
- return Response(stringify(response),
- mimetype='application/json')
-
-
-def err(e) -> Response:
- error = {
- 'type': e.__class__.__name__,
- 'message': e.message if hasattr(e, 'message') else str(e)
- }
- if is_development_mode():
- tb = format_tb(e)
- if tb:
- error['stacktrace'] = tb
- data = {
- 'result': 'error',
- 'error': error
- }
- return Response(stringify(data), mimetype='application/json')
-
-
-def get_token() -> Optional[str]:
- name = 'X-Token'
- if name in request.headers:
- return request.headers[name]
-
- token = request.args.get('token', default='', type=str)
- if token != '':
- return token
-
- return None
-
-
-@app.errorhandler(Exception)
-def handle_exception(e):
- if isinstance(e, HTTPException):
- return e
- return err(e), 500
-
-
-@app.before_request
-def validate_token() -> None:
- if request.path.startswith('/api/') and not is_development_mode():
- token = get_token()
- if not token:
- raise AuthError(f'token is missing')
-
- if token != config['api']['token']:
- raise AuthError('invalid token')
-
-
-def get_app():
- global db, sensors_db
-
- config.load('web_api')
- app.config.from_mapping(**config['flask'])
-
- db = BotsDatabase()
- sensors_db = SensorsDatabase()
-
- return app