From 02737bf9b4801c53f480f8cd135fec54fd6cf782 Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Mon, 28 Oct 2024 00:10:56 +0800 Subject: [PATCH] added command line arguments --- Dockerfile | 2 +- src/bugis/__main__.py | 3 +- src/bugis/asgi.py | 35 ++---------- src/bugis/cli.py | 83 +++++++++++++++++++++++++++-- src/bugis/configuration.py | 21 ++++++-- src/bugis/default-conf/logging.yaml | 27 ++++------ src/bugis/server.py | 3 +- 7 files changed, 115 insertions(+), 59 deletions(-) diff --git a/Dockerfile b/Dockerfile index d7f3ceb..f311702 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,7 +36,7 @@ ENV GRANIAN_PORT=8000 ENV GRANIAN_INTERFACE=asgi ENV GRANIAN_LOOP=asyncio ENV GRANIAN_LOG_ENABLED=false - +ENV GRANIAN_LOG_ACCESS_ENABLED=true ENTRYPOINT ["/var/lib/bugis/.venv/bin/python", "-m", "granian", "bugis.asgi:application"] EXPOSE 8000/tcp diff --git a/src/bugis/__main__.py b/src/bugis/__main__.py index f7ad4a5..f260a1e 100644 --- a/src/bugis/__main__.py +++ b/src/bugis/__main__.py @@ -1,5 +1,4 @@ -import argparse -from cli import main +from .cli import main import sys main(sys.argv[1:]) diff --git a/src/bugis/asgi.py b/src/bugis/asgi.py index 8d59258..5913c71 100644 --- a/src/bugis/asgi.py +++ b/src/bugis/asgi.py @@ -1,20 +1,11 @@ import logging -from logging.config import dictConfig as configure_logging - -from yaml import safe_load - -from .configuration import Configuration - -with open(Configuration.instance.logging_configuration_file, 'r') as input_file: - conf = safe_load(input_file) - configure_logging(conf) - +from asyncio import get_running_loop +from typing import Optional, Awaitable, Callable, Any, Mapping from pwo import Maybe + from .server import Server -from asyncio import get_running_loop -from .asgi_utils import decode_headers -from typing import Optional, Awaitable, Callable, Any, Mapping + log = logging.getLogger('access') log.propagate = False @@ -33,22 +24,6 @@ async def application(scope, receive, send : Callable[[Mapping[str, Any]], Await await _server.stop() await send({'type': 'lifespan.shutdown.complete'}) else: - def maybe_log(evt): - d = { - 'response_headers': (Maybe.of_nullable(evt.get('headers')) - .map(decode_headers) - .or_none()), - 'status': evt['status'] - } - log.info(None, extra=dict(**{k : v for k, v in d.items() if k is not None}, **scope)) - def wrapped_send(*args, **kwargs): - result = send(*args, **kwargs) - (Maybe.of(args) - .filter(lambda it: len(it) > 0) - .map(lambda it: it[0]) - .filter(lambda it: it.get('type') == 'http.response.start') - .if_present(maybe_log)) - return result pathsend = (Maybe.of_nullable(scope.get('extensions')) .map(lambda it: it.get("http.response.pathsend")) .is_present) @@ -61,7 +36,7 @@ async def application(scope, receive, send : Callable[[Mapping[str, Any]], Await .map(lambda it: it.decode()) .or_else(None), Maybe.of_nullable(scope.get('query_string', None)).map(lambda it: it.decode()).or_else(None), - wrapped_send, + send, pathsend ) diff --git a/src/bugis/cli.py b/src/bugis/cli.py index 725d30f..4dead5b 100644 --- a/src/bugis/cli.py +++ b/src/bugis/cli.py @@ -9,7 +9,7 @@ from granian import Granian from pwo import Maybe from .configuration import Configuration - +from granian.constants import HTTPModes, Interfaces, ThreadModes, Loops def main(args: Optional[Sequence[str]] = None): parser = argparse.ArgumentParser(description="A simple CLI program to render Markdown files") @@ -27,15 +27,88 @@ def main(args: Optional[Sequence[str]] = None): default=default_configuration_file, type=Path, ) + parser.add_argument( + '-a', + '--address', + help='Server bind address', + default='127.0.0.1', + ) + parser.add_argument( + '-p', + '--port', + help='Server port', + default='8000', + type=int + ) + parser.add_argument( + '--access-log', + help='Enable access log', + action='store_true', + dest='log_access' + ) + parser.add_argument( + '--logging-configuration', + help='Logging configuration file', + dest='log_config_file' + ) + parser.add_argument( + '-w', '--workers', + help='Number of worker processes', + default='1', + dest='workers', + type = int + ) + parser.add_argument( + '-t', '--threads', + help='Number of threads per worker', + default='1', + dest='threads', + type=int + ) + parser.add_argument( + '--http', + help='HTTP protocol version', + dest='http', + type=lambda it: HTTPModes(it), + choices=[str(mode) for mode in HTTPModes], + default = 'auto', + ) + parser.add_argument( + '--threading-mode', + help='Threading mode', + dest='threading_mode', + type=lambda it: ThreadModes(it), + choices=[str(mode) for mode in ThreadModes], + default=ThreadModes.workers + ) + parser.add_argument( + '--loop', + help='Loop', + dest='loop', + type=lambda it: Loops(it), + choices=[str(mode) for mode in Loops] + ) args = parser.parse_args(args) - def parse(configuration: Path): with open(configuration, 'r') as f: - return Configuration.from_dict(yaml.safe_load(f)) + return yaml.safe_load(f) - conf = Maybe.of_nullable(args.configuration).map(parse).or_else(Configuration.instance) + def assign(it: Configuration): + Configuration.instance = it + Maybe.of_nullable(args.configuration).map(parse).if_present(assign) + conf = Configuration.instance + + + granian_conf = asdict(conf).setdefault('granian', dict()) + for k, v in vars(args).items(): + if v is not None: + granian_conf[k] = v + if args.log_config_file: + with open(args.log_config_file, 'r') as f: + granian_conf['log_dictconfig'] = yaml.safe_load(f) + granian_conf = Configuration.GranianConfiguration.from_dict(granian_conf) Granian( "bugis.asgi:application", - **asdict(conf.granian) + **asdict(granian_conf) ).serve() \ No newline at end of file diff --git a/src/bugis/configuration.py b/src/bugis/configuration.py index 072c89c..8edf9eb 100644 --- a/src/bugis/configuration.py +++ b/src/bugis/configuration.py @@ -1,6 +1,8 @@ from os import environ from pathlib import Path from dataclasses import dataclass, field, asdict + +import yaml from granian.constants import Loops, Interfaces, ThreadModes, HTTPModes, StrEnum from granian.log import LogLevels from granian.http import HTTP1Settings, HTTP2Settings @@ -8,16 +10,27 @@ from typing import Optional, Sequence, Dict, Any from pwo import classproperty, Maybe from yaml import add_representer, SafeDumper, SafeLoader +def parse_log_config(conf_file=None) -> Dict[str, Any]: + if conf_file is None: + conf_file = environ.get("LOGGING_CONFIGURATION_FILE", + Path(__file__).parent / 'default-conf' / 'logging.yaml') + with open(conf_file, 'r') as file: + return yaml.safe_load(file) @dataclass(frozen=True) class Configuration: - logging_configuration_file: str = environ.get("LOGGING_CONFIGURATION_FILE", - Path(__file__).parent / 'default-conf' / 'logging.yaml') plant_uml_server_address: str = environ.get('PLANT_UML_SERVER_ADDRESS', None) + _instance = None @classproperty def instance(cls) -> 'Configuration': - return Configuration() + if cls._instance is None: + cls._instance = Configuration() + return cls._instance + + @instance.setter + def bar(cls, value): + cls._instance = value @dataclass(frozen=True) class GranianConfiguration: @@ -77,7 +90,7 @@ class Configuration: http2_settings=Maybe.of_nullable(d.get('http2_settings')).map(lambda it: HTTP2Settings(**it)).or_else(None), log_enabled=d.get('log_enabled', None), log_level=Maybe.of_nullable(d.get('log_level')).map(lambda it: LogLevels(it)).or_else(None), - # log_dictconfig: Optional[Dict[str, Any]] = None, + log_dictconfig=parse_log_config(d.get('log_config_file')), log_access=d.get('log_access', None), log_access_format=d.get('log_access_format', None), ssl_cert=d.get('ssl_cert', None), diff --git a/src/bugis/default-conf/logging.yaml b/src/bugis/default-conf/logging.yaml index 54c12d0..6f66f1a 100644 --- a/src/bugis/default-conf/logging.yaml +++ b/src/bugis/default-conf/logging.yaml @@ -1,5 +1,5 @@ version: 1 -disable_existing_loggers: True +disable_existing_loggers: False handlers: console: class : logging.StreamHandler @@ -8,28 +8,23 @@ handlers: stream : ext://sys.stderr access: class : logging.StreamHandler - formatter: request + formatter: access level : INFO - stream : ext://sys.stderr + stream : ext://sys.stdout formatters: - brief: - format: '%(message)s' default: - format: '{asctime} [{levelname}] ({processName:s}/{threadName:s}) - {name} - {message}' - style: '{' - datefmt: '%Y-%m-%d %H:%M:%S' - request: - format: '{asctime} {client[0]}:{client[1]} HTTP/{http_version} {method} {path} - {status}' + format: '{asctime}.{msecs:0<3.0f} [{levelname}] ({processName:s}/{threadName:s}) - {name} - {message}' style: '{' datefmt: '%Y-%m-%d %H:%M:%S' + access: + format: '%(message)s' loggers: root: handlers: [console] - level: DEBUG - access: - handlers: [access] + _granian: level: INFO - watchdog.observers.inotify_buffer: + propagate: False + granian.access: + handlers: [ access ] level: INFO - MARKDOWN: - level: INFO \ No newline at end of file + propagate: False diff --git a/src/bugis/server.py b/src/bugis/server.py index b58ed54..071f189 100644 --- a/src/bugis/server.py +++ b/src/bugis/server.py @@ -19,6 +19,7 @@ from pwo import Maybe from .asgi_utils import encode_headers from .async_watchdog import FileWatcher +from .configuration import Configuration from .md2html import compile_html, load_from_cache, STATIC_RESOURCES, MARDOWN_EXTENSIONS from .plantuml import render_plant_uml @@ -179,7 +180,7 @@ class Server: 'type': 'http.response.body', 'body': body }) - elif is_plant_uml(path): + elif Configuration.instance.plant_uml_server_address and is_plant_uml(path): logger.debug("Starting PlantUML rendering for file '%s'", path) logger.debug("Completed PlantUML rendering for file '%s'", path) await send({