Set up the bones of a Flask app with configuration
This commit is contained in:
parent
0909fd7e99
commit
a7fdd6cbd0
7 changed files with 131 additions and 1 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,5 +1,6 @@
|
||||||
*.pyc
|
*.pyc
|
||||||
__pycache__/*
|
__pycache__/*
|
||||||
.env
|
.env
|
||||||
|
config.json
|
||||||
result
|
result
|
||||||
todo.md
|
todo.md
|
||||||
|
|
|
||||||
7
config.example.json
Normal file
7
config.example.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 8080,
|
||||||
|
"logging": {
|
||||||
|
"level": "info"
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
38
src/config/__init__.py
Normal file
38
src/config/__init__.py
Normal file
|
|
@ -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']
|
||||||
32
src/config/logging.py
Normal file
32
src/config/logging.py
Normal file
|
|
@ -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']])
|
||||||
20
src/config/parse.py
Normal file
20
src/config/parse.py
Normal file
|
|
@ -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}')
|
||||||
34
src/main.py
34
src/main.py
|
|
@ -1,3 +1,35 @@
|
||||||
#!/usr/bin/env python
|
#!/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()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue