Set up the bones of a Flask app with configuration

This commit is contained in:
Campbell Alden 2026-07-30 22:45:25 +09:00
parent 0909fd7e99
commit a7fdd6cbd0
7 changed files with 131 additions and 1 deletions

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
*.pyc *.pyc
__pycache__/* __pycache__/*
.env .env
config.json
result result
todo.md todo.md

7
config.example.json Normal file
View file

@ -0,0 +1,7 @@
{
"host": "0.0.0.0",
"port": 8080,
"logging": {
"level": "info"
}
}

0
src/__init__.py Normal file
View file

38
src/config/__init__.py Normal file
View 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
View 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
View 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}')

View file

@ -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()