diff --git a/.gitignore b/.gitignore
index 033df5f..1c8c959 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
.venv
__pycache__
+*.pyc
+src/bugis/_version.py
diff --git a/Dockerfile b/Dockerfile
index 83392cb..0e54f3a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,18 +4,20 @@ RUN --mount=type=cache,target=/var/cache/apk apk update
RUN --mount=type=cache,target=/var/cache/apk apk add python3 py3-pip graphviz
FROM base AS build
-RUN --mount=type=cache,target=/var/cache/apk apk add musl-dev gcc graphviz-dev
+RUN --mount=type=cache,target=/var/cache/apk apk add musl-dev gcc graphviz-dev git
RUN adduser -D luser
USER luser
WORKDIR /home/luser
-COPY --chown=luser:users ./requirements-dev.txt ./bugis/requirements-dev.txt
-COPY --chown=luser:users ./src ./bugis/src
-COPY --chown=luser:users ./pyproject.toml ./bugis/pyproject.toml
-WORKDIR /home/luser/bugis
+COPY --chown=luser:users ./requirements-dev.txt ./requirements-dev.txt
+COPY --chown=luser:users ./requirements-dev.txt ./requirements-run.txt
+WORKDIR /home/luser/
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 install -r requirements-dev.txt /home/luser/wheel/*.whl
-RUN --mount=type=cache,target=/home/luser/.cache/pip,uid=1000,gid=1000 .venv/bin/python -m build
+COPY --chown=luser:users . /home/luser/bugis
+WORKDIR /home/luser/bugis
+RUN rm -rf .venv dist build
+RUN --mount=type=cache,target=/home/luser/.cache/pip,uid=1000,gid=1000 /home/luser/.venv/bin/python -m build
FROM base AS release
RUN mkdir /srv/http
@@ -24,7 +26,7 @@ USER bugis
WORKDIR /var/bugis
COPY --chown=bugis:users conf/pip.conf ./.pip/pip.conf
RUN python -m venv .venv
-RUN --mount=type=cache,target=/var/bugis/.cache/pip,uid=1000,gid=1000 --mount=type=bind,ro,source=./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/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
VOLUME /srv/http
WORKDIR /srv/http
@@ -32,7 +34,6 @@ WORKDIR /srv/http
ENV GRANIAN_HOST=0.0.0.0
ENV GRANIAN_INTERFACE=asginl
ENV GRANIAN_LOOP=asyncio
-ENV GRANIAN_LOOP=asyncio
ENV GRANIAN_LOG_ENABLED=false
ENTRYPOINT ["/var/bugis/.venv/bin/python", "-m", "granian", "bugis.asgi:application"]
diff --git a/pyproject.toml b/pyproject.toml
index 5985373..669fb56 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -58,3 +58,4 @@ exclude = ["scripts", "docs", "test"]
strict = true
[tool.setuptools_scm]
+version_file = "src/bugis/_version.py"
\ No newline at end of file
diff --git a/src/bugis/asgi.py b/src/bugis/asgi.py
index 3f7c5bd..e047c29 100644
--- a/src/bugis/asgi.py
+++ b/src/bugis/asgi.py
@@ -13,7 +13,8 @@ with open(logging_configuration_file, 'r') as input_file:
configure_logging(conf)
-log = logging.getLogger(__name__)
+log = logging.getLogger('access')
+log.propagate = False
_server = None
async def application(ctx, receive, send):
diff --git a/src/bugis/async_watchdog.py b/src/bugis/async_watchdog.py
index 3d1ce38..f5b9b17 100644
--- a/src/bugis/async_watchdog.py
+++ b/src/bugis/async_watchdog.py
@@ -1,4 +1,5 @@
import asyncio
+import logging
from watchdog.events import FileSystemEventHandler, FileSystemEvent, PatternMatchingEventHandler
from watchdog.observers import Observer
@@ -8,6 +9,7 @@ from asyncio import Queue, AbstractEventLoop, Future, CancelledError, Task
from typing import Optional, Callable
from logging import getLogger
+log : logging.Logger = getLogger(__name__)
class Subscription:
_unsubscribe_callback: Callable[['Subscription'], None]
@@ -21,10 +23,12 @@ class Subscription:
def unsubscribe(self) -> None:
self._unsubscribe_callback(self)
+ log.debug('Deleted subscription %s', id(self))
async def wait(self, tout: float) -> bool:
handle = self._loop.call_later(tout, lambda: self._event.cancel())
try:
+ log.debug('Subscription %s is waiting for an event', id(self))
await self._event
return True
except CancelledError:
@@ -33,6 +37,7 @@ class Subscription:
handle.cancel()
def notify(self) -> None:
+ log.debug('Subscription %s notified', id(self))
self._event.set_result(None)
def reset(self) -> None:
@@ -85,7 +90,6 @@ def watch(path: Path, queue: Queue, loop: AbstractEventLoop,
observer.join()
loop.call_soon_threadsafe(queue.put_nowait, None)
-
class SubscriptionManager:
_loop: AbstractEventLoop
_queue: Queue
@@ -104,6 +108,7 @@ class SubscriptionManager:
subscriptions_per_path.remove(subscription)
result = Subscription(unsubscribe_callback, self._loop)
+ log.debug('Created subscription %s to path %s', id(result), path)
subscriptions_per_path.add(result)
return result
diff --git a/src/bugis/default-conf/logging.yaml b/src/bugis/default-conf/logging.yaml
index 47d5818..4d9ea47 100644
--- a/src/bugis/default-conf/logging.yaml
+++ b/src/bugis/default-conf/logging.yaml
@@ -4,8 +4,13 @@ handlers:
console:
class : logging.StreamHandler
formatter: default
+ level : DEBUG
+ stream : ext://sys.stderr
+ access:
+ class : logging.StreamHandler
+ formatter: request
level : INFO
- stream : ext://sys.stdout
+ stream : ext://sys.stderr
formatters:
brief:
format: '%(message)s'
@@ -13,7 +18,14 @@ formatters:
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}'
+ style: '{'
+ datefmt: '%Y-%m-%d %H:%M:%S'
loggers:
root:
handlers: [console]
+ level: DEBUG
+ access:
+ handlers: [access]
level: INFO
\ No newline at end of file
diff --git a/src/bugis/md2html.py b/src/bugis/md2html.py
index be8507a..fac045e 100644
--- a/src/bugis/md2html.py
+++ b/src/bugis/md2html.py
@@ -2,7 +2,7 @@ import sys
from os.path import dirname, join, relpath
from time import time
from typing import Optional, TYPE_CHECKING
-
+from aiofiles import open as async_open
import markdown
if TYPE_CHECKING:
@@ -20,21 +20,21 @@ STATIC_CACHE: dict[str, tuple[str, float]] = {}
MARDOWN_EXTENSIONS = ['extra', 'smarty', 'tables', 'codehilite']
-def load_from_cache(path) -> tuple[str, float]:
+async def load_from_cache(path) -> tuple[str, float]:
global STATIC_CACHE
if path not in STATIC_CACHE:
- with open(join(dirname(__file__), 'static') + path, 'r') as static_file:
- STATIC_CACHE[path] = (static_file.read(), time())
+ async with async_open(join(dirname(__file__), 'static') + path, 'r') as static_file:
+ STATIC_CACHE[path] = (await static_file.read(), time())
return STATIC_CACHE[path]
-def compile_html(url_path,
+async def compile_html(url_path,
mdfile: 'StrOrBytesPath',
prefix: Optional['StrOrBytesPath'] = None,
extensions: Optional[list[str]] = None,
raw: bool = False) -> str:
- with mdfile and open(mdfile, 'r') or sys.stdin as instream:
- html = markdown.markdown(instream.read(), extensions=extensions, output_format='html')
+ async with mdfile and async_open(mdfile, 'r') or sys.stdin as instream:
+ html = markdown.markdown(await instream.read(), extensions=extensions, output_format='html')
if raw:
doc = html
else:
@@ -44,5 +44,5 @@ def compile_html(url_path,
css = f''
for css_file in ('github-markdown.css', 'pygment.css', 'custom.css'):
css += f' '
- doc = load_from_cache('/template.html')[0].format(content=html, script=script, css=css)
+ doc = (await load_from_cache('/template.html'))[0].format(content=html, script=script, css=css)
return doc
diff --git a/src/bugis/server.py b/src/bugis/server.py
index 59fbf47..fa61019 100644
--- a/src/bugis/server.py
+++ b/src/bugis/server.py
@@ -70,12 +70,12 @@ class Server:
url_path: 'StrOrBytesPath' = normpath(join('/', relative_path))
path: 'StrOrBytesPath' = join(self.root_dir, relative_path)
if url_path in STATIC_RESOURCES:
- content, mtime = load_from_cache(url_path)
+ content, mtime = await load_from_cache(url_path)
content = content.encode()
etag, digest = await self.compute_etag_and_digest(
etag,
url_path,
- lambda: AiofilesContextManager(completed_future(AsyncBufferedReader(BytesIO(content), loop=get_running_loop()))),
+ lambda: AiofilesContextManager(completed_future(AsyncBufferedReader(BytesIO(content), loop=get_running_loop(), executor=None))),
lambda: completed_future(mtime)
)
if etag and etag == digest:
@@ -267,11 +267,11 @@ class Server:
raw: bool,
digest: str,
send) -> list[bytes]:
- body = compile_html(url_path,
+ body = (await compile_html(url_path,
path,
self.prefix,
MARDOWN_EXTENSIONS,
- raw=raw).encode()
+ raw=raw)).encode()
await send({
'type': 'http.response.start',
'status': 200,