diff --git a/.gitignore b/.gitignore index 12a0ed5..ebb8a8d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.pyc __pycache__/* .env +config.json result todo.md diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..58cf7d7 --- /dev/null +++ b/config.example.json @@ -0,0 +1,7 @@ +{ + "host": "0.0.0.0", + "port": 8080, + "logging": { + "level": "info" + } +} diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/config/__init__.py b/src/config/__init__.py new file mode 100644 index 0000000..7b09571 --- /dev/null +++ b/src/config/__init__.py @@ -0,0 +1,38 @@ +import json +from logging import INFO +from dataclasses import dataclass +from typing import Any + +from .logging import Logging +from .parse import ConfigParseError, assert_key_of_type + + +@dataclass +class Config: + host: str | None + port: int | None + logging: Logging + + @classmethod + def from_dict(cls, config: dict[str, Any]) -> 'Config': + assert_key_of_type(config, 'logging', dict) + assert_key_of_type(config, 'host', str) + assert_key_of_type(config, 'port', int) + try: + log_config = Logging.from_dict(config['logging']) + return Config(host=config['host'], port=config['port'], logging=log_config) + + except ConfigParseError as e: + raise ConfigParseError(['logging', *e.keypath], e.issue) from e + + +DEFAULT_CONFIG = Config(logging=Logging(level=INFO), port=None, host=None) + + +def parse_config(filename: str) -> Config: + with open(filename, 'r') as infile: + config = json.loads(infile.read()) + return Config.from_dict(config) + + +__all__ = ['Config', 'parse_config', 'DEFAULT_CONFIG'] diff --git a/src/config/logging.py b/src/config/logging.py new file mode 100644 index 0000000..43a3071 --- /dev/null +++ b/src/config/logging.py @@ -0,0 +1,32 @@ +from typing import Any +from dataclasses import dataclass +import logging + +from .parse import assert_key_of_type, ConfigParseError + +LOG_LEVELS = { + 'critical': logging.CRITICAL, + 'fatal': logging.FATAL, + 'error': logging.ERROR, + 'warning': logging.WARNING, + 'warn': logging.WARN, + 'info': logging.INFO, + 'debug': logging.DEBUG, + 'notset': logging.NOTSET, +} + + +@dataclass +class Logging: + level: int + + @classmethod + def from_dict(cls, config: dict[str, Any]) -> 'Logging': + assert_key_of_type(config, 'level', str) + log_level_setting = config['level'] + if log_level_setting not in LOG_LEVELS: + raise ConfigParseError( + ['level'], f'unknown log level "{log_level_setting}". Expected one of: {", ".join(LOG_LEVELS.keys())}' + ) + + return Logging(level=LOG_LEVELS[config['level']]) diff --git a/src/config/parse.py b/src/config/parse.py new file mode 100644 index 0000000..c940175 --- /dev/null +++ b/src/config/parse.py @@ -0,0 +1,20 @@ +from typing import Any + + +class ConfigParseError(Exception): + """An error for when parsing a config fails""" + + def __init__(self, keypath: list[str], issue: str): + path = '.'.join(keypath) + message = f'{path}: {issue}' + super().__init__(message) + self.keypath = keypath + self.issue = issue + + +def assert_key_of_type(config: dict[str, Any], key: str, kind: Any): + if key not in config: + raise ConfigParseError([key], 'missing') + + if not isinstance(config[key], kind): + raise ConfigParseError([key], f'type of "{key}" was {type(config[key])}, expected {kind}') diff --git a/src/main.py b/src/main.py index 536cb12..2203566 100644 --- a/src/main.py +++ b/src/main.py @@ -1,3 +1,35 @@ #!/usr/bin/env python -print('hello, world!') +from waitress import serve +from flask import Flask +import argparse +import logging + +from src.config import parse_config, DEFAULT_CONFIG, Config + +logger = logging.getLogger(__name__) + + +def create_app(name: str, config: Config) -> Flask: + logging.basicConfig(level=config.logging.level, format=('%(asctime)s %(levelname)s [%(name)s] %(message)s')) + + app = Flask(name) + + if config.host: + app.config['SERVER_NAME'] = config.host + + return app + + +def main(): + parser = argparse.ArgumentParser('Cereal', description='run the Cereal server') + parser.add_argument('--config', '-c', help='The path to the configuration file, expects JSON') + args = parser.parse_args() + config = parse_config(args.config) if args.config else DEFAULT_CONFIG + app = create_app('Cereal', config) + logger.info(str(config)) + serve(app, host='0.0.0.0' if config.host is None else config.host, port=config.port) + + +if __name__ == '__main__': + main()