2 Commits
0.2.0 ... 0.2.2

Author SHA1 Message Date
02737bf9b4 added command line arguments
All checks were successful
CI / Build Pip package (push) Successful in 18s
CI / Build Docker image (push) Successful in 1m2s
2024-10-28 00:10:56 +08:00
29bdad09bf reduced Docker image size
All checks were successful
CI / Build Pip package (push) Successful in 16s
CI / Build Docker image (push) Successful in 3m6s
2024-10-25 07:25:13 +08:00
7 changed files with 120 additions and 64 deletions

View File

@@ -12,7 +12,7 @@ COPY --chown=luser:users ./requirements-dev.txt ./requirements-dev.txt
COPY --chown=luser:users ./requirements-run.txt ./requirements-run.txt COPY --chown=luser:users ./requirements-run.txt ./requirements-run.txt
WORKDIR /home/luser/ WORKDIR /home/luser/
RUN python -m venv .venv RUN python -m venv .venv
RUN --mount=type=cache,target=/home/luser/.cache/pip,uid=1000,gid=1000 .venv/bin/pip wheel -w /home/luser/wheel -r requirements-dev.txt pygraphviz RUN --mount=type=cache,target=/home/luser/.cache/pip,uid=1000,gid=1000 .venv/bin/pip wheel -w /home/luser/wheel pygraphviz
RUN --mount=type=cache,target=/home/luser/.cache/pip,uid=1000,gid=1000 .venv/bin/pip install -r requirements-dev.txt /home/luser/wheel/*.whl RUN --mount=type=cache,target=/home/luser/.cache/pip,uid=1000,gid=1000 .venv/bin/pip install -r requirements-dev.txt /home/luser/wheel/*.whl
COPY --chown=luser:users . /home/luser/bugis COPY --chown=luser:users . /home/luser/bugis
WORKDIR /home/luser/bugis WORKDIR /home/luser/bugis
@@ -21,12 +21,12 @@ RUN --mount=type=cache,target=/home/luser/.cache/pip,uid=1000,gid=1000 /home/lus
FROM base AS release FROM base AS release
RUN mkdir /srv/http RUN mkdir /srv/http
RUN adduser -D -h /var/bugis -u 1000 bugis RUN adduser -D -h /var/lib/bugis -u 1000 bugis
USER bugis USER bugis
WORKDIR /var/bugis WORKDIR /var/lib/bugis
COPY --chown=bugis:users conf/pip.conf ./.pip/pip.conf COPY --chown=bugis:users conf/pip.conf ./.pip/pip.conf
RUN python -m venv .venv RUN python -m venv .venv
RUN --mount=type=cache,target=/var/bugis/.cache/pip,uid=1000,gid=1000 --mount=type=bind,ro,from=build,source=/home/luser/bugis/requirements-run.txt,target=/requirements-run.txt --mount=type=bind,ro,from=build,source=/home/luser/wheel,target=/wheel .venv/bin/pip install -r /requirements-run.txt /wheel/*.whl RUN --mount=type=cache,target=/var/bugis/.cache/pip,uid=1000,gid=1000 --mount=type=bind,ro,from=build,source=/home/luser/requirements-run.txt,target=/requirements-run.txt --mount=type=bind,ro,from=build,source=/home/luser/wheel,target=/wheel .venv/bin/pip install -r /requirements-run.txt /wheel/*.whl
RUN --mount=type=cache,target=/var/bugis/.cache/pip,uid=1000,gid=1000 --mount=type=bind,ro,from=build,source=/home/luser/bugis/dist,target=/dist .venv/bin/pip install /dist/*.whl RUN --mount=type=cache,target=/var/bugis/.cache/pip,uid=1000,gid=1000 --mount=type=bind,ro,from=build,source=/home/luser/bugis/dist,target=/dist .venv/bin/pip install /dist/*.whl
VOLUME /srv/http VOLUME /srv/http
WORKDIR /srv/http WORKDIR /srv/http
@@ -36,7 +36,7 @@ ENV GRANIAN_PORT=8000
ENV GRANIAN_INTERFACE=asgi ENV GRANIAN_INTERFACE=asgi
ENV GRANIAN_LOOP=asyncio ENV GRANIAN_LOOP=asyncio
ENV GRANIAN_LOG_ENABLED=false ENV GRANIAN_LOG_ENABLED=false
ENV GRANIAN_LOG_ACCESS_ENABLED=true
ENTRYPOINT ["/var/bugis/.venv/bin/python", "-m", "granian", "bugis.asgi:application"] ENTRYPOINT ["/var/lib/bugis/.venv/bin/python", "-m", "granian", "bugis.asgi:application"]
EXPOSE 8000/tcp EXPOSE 8000/tcp

View File

@@ -1,5 +1,4 @@
import argparse from .cli import main
from cli import main
import sys import sys
main(sys.argv[1:]) main(sys.argv[1:])

View File

@@ -1,20 +1,11 @@
import logging import logging
from logging.config import dictConfig as configure_logging from asyncio import get_running_loop
from typing import Optional, Awaitable, Callable, Any, Mapping
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 pwo import Maybe from pwo import Maybe
from .server import Server 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 = logging.getLogger('access')
log.propagate = False log.propagate = False
@@ -33,22 +24,6 @@ async def application(scope, receive, send : Callable[[Mapping[str, Any]], Await
await _server.stop() await _server.stop()
await send({'type': 'lifespan.shutdown.complete'}) await send({'type': 'lifespan.shutdown.complete'})
else: 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')) pathsend = (Maybe.of_nullable(scope.get('extensions'))
.map(lambda it: it.get("http.response.pathsend")) .map(lambda it: it.get("http.response.pathsend"))
.is_present) .is_present)
@@ -61,7 +36,7 @@ async def application(scope, receive, send : Callable[[Mapping[str, Any]], Await
.map(lambda it: it.decode()) .map(lambda it: it.decode())
.or_else(None), .or_else(None),
Maybe.of_nullable(scope.get('query_string', None)).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 pathsend
) )

View File

@@ -9,7 +9,7 @@ from granian import Granian
from pwo import Maybe from pwo import Maybe
from .configuration import Configuration from .configuration import Configuration
from granian.constants import HTTPModes, Interfaces, ThreadModes, Loops
def main(args: Optional[Sequence[str]] = None): def main(args: Optional[Sequence[str]] = None):
parser = argparse.ArgumentParser(description="A simple CLI program to render Markdown files") 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, default=default_configuration_file,
type=Path, 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) args = parser.parse_args(args)
def parse(configuration: Path): def parse(configuration: Path):
with open(configuration, 'r') as f: 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( Granian(
"bugis.asgi:application", "bugis.asgi:application",
**asdict(conf.granian) **asdict(granian_conf)
).serve() ).serve()

View File

@@ -1,6 +1,8 @@
from os import environ from os import environ
from pathlib import Path from pathlib import Path
from dataclasses import dataclass, field, asdict from dataclasses import dataclass, field, asdict
import yaml
from granian.constants import Loops, Interfaces, ThreadModes, HTTPModes, StrEnum from granian.constants import Loops, Interfaces, ThreadModes, HTTPModes, StrEnum
from granian.log import LogLevels from granian.log import LogLevels
from granian.http import HTTP1Settings, HTTP2Settings from granian.http import HTTP1Settings, HTTP2Settings
@@ -8,16 +10,27 @@ from typing import Optional, Sequence, Dict, Any
from pwo import classproperty, Maybe from pwo import classproperty, Maybe
from yaml import add_representer, SafeDumper, SafeLoader 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) @dataclass(frozen=True)
class Configuration: 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) plant_uml_server_address: str = environ.get('PLANT_UML_SERVER_ADDRESS', None)
_instance = None
@classproperty @classproperty
def instance(cls) -> 'Configuration': 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) @dataclass(frozen=True)
class GranianConfiguration: 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), 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_enabled=d.get('log_enabled', None),
log_level=Maybe.of_nullable(d.get('log_level')).map(lambda it: LogLevels(it)).or_else(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=d.get('log_access', None),
log_access_format=d.get('log_access_format', None), log_access_format=d.get('log_access_format', None),
ssl_cert=d.get('ssl_cert', None), ssl_cert=d.get('ssl_cert', None),

View File

@@ -1,5 +1,5 @@
version: 1 version: 1
disable_existing_loggers: True disable_existing_loggers: False
handlers: handlers:
console: console:
class : logging.StreamHandler class : logging.StreamHandler
@@ -8,28 +8,23 @@ handlers:
stream : ext://sys.stderr stream : ext://sys.stderr
access: access:
class : logging.StreamHandler class : logging.StreamHandler
formatter: request formatter: access
level : INFO level : INFO
stream : ext://sys.stderr stream : ext://sys.stdout
formatters: formatters:
brief:
format: '%(message)s'
default: default:
format: '{asctime} [{levelname}] ({processName:s}/{threadName:s}) - {name} - {message}' format: '{asctime}.{msecs:0<3.0f} [{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}'
style: '{' style: '{'
datefmt: '%Y-%m-%d %H:%M:%S' datefmt: '%Y-%m-%d %H:%M:%S'
access:
format: '%(message)s'
loggers: loggers:
root: root:
handlers: [console] handlers: [console]
level: DEBUG _granian:
access: level: INFO
propagate: False
granian.access:
handlers: [ access ] handlers: [ access ]
level: INFO level: INFO
watchdog.observers.inotify_buffer: propagate: False
level: INFO
MARKDOWN:
level: INFO

View File

@@ -19,6 +19,7 @@ from pwo import Maybe
from .asgi_utils import encode_headers from .asgi_utils import encode_headers
from .async_watchdog import FileWatcher from .async_watchdog import FileWatcher
from .configuration import Configuration
from .md2html import compile_html, load_from_cache, STATIC_RESOURCES, MARDOWN_EXTENSIONS from .md2html import compile_html, load_from_cache, STATIC_RESOURCES, MARDOWN_EXTENSIONS
from .plantuml import render_plant_uml from .plantuml import render_plant_uml
@@ -179,7 +180,7 @@ class Server:
'type': 'http.response.body', 'type': 'http.response.body',
'body': 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("Starting PlantUML rendering for file '%s'", path)
logger.debug("Completed PlantUML rendering for file '%s'", path) logger.debug("Completed PlantUML rendering for file '%s'", path)
await send({ await send({