fixed async file reads
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
|||||||
.venv
|
.venv
|
||||||
__pycache__
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
src/bugis/_version.py
|
||||||
|
@@ -4,17 +4,19 @@ RUN --mount=type=cache,target=/var/cache/apk apk update
|
|||||||
RUN --mount=type=cache,target=/var/cache/apk apk add python3 py3-pip graphviz
|
RUN --mount=type=cache,target=/var/cache/apk apk add python3 py3-pip graphviz
|
||||||
|
|
||||||
FROM base AS build
|
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
|
RUN adduser -D luser
|
||||||
USER luser
|
USER luser
|
||||||
WORKDIR /home/luser
|
WORKDIR /home/luser
|
||||||
COPY --chown=luser:users ./requirements-dev.txt ./bugis/requirements-dev.txt
|
COPY --chown=luser:users ./requirements-dev.txt ./bugis/requirements-dev.txt
|
||||||
|
COPY --chown=luser:users ./requirements-dev.txt ./bugis/requirements-run.txt
|
||||||
COPY --chown=luser:users ./src ./bugis/src
|
COPY --chown=luser:users ./src ./bugis/src
|
||||||
COPY --chown=luser:users ./pyproject.toml ./bugis/pyproject.toml
|
COPY --chown=luser:users ./pyproject.toml ./bugis/pyproject.toml
|
||||||
WORKDIR /home/luser/bugis
|
WORKDIR /home/luser/bugis
|
||||||
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 -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/pip install -r requirements-dev.txt /home/luser/wheel/*.whl
|
||||||
|
COPY --chown=luser:users .git .git
|
||||||
RUN --mount=type=cache,target=/home/luser/.cache/pip,uid=1000,gid=1000 .venv/bin/python -m build
|
RUN --mount=type=cache,target=/home/luser/.cache/pip,uid=1000,gid=1000 .venv/bin/python -m build
|
||||||
|
|
||||||
FROM base AS release
|
FROM base AS release
|
||||||
@@ -24,7 +26,7 @@ USER bugis
|
|||||||
WORKDIR /var/bugis
|
WORKDIR /var/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,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
|
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
|
||||||
@@ -32,7 +34,6 @@ WORKDIR /srv/http
|
|||||||
ENV GRANIAN_HOST=0.0.0.0
|
ENV GRANIAN_HOST=0.0.0.0
|
||||||
ENV GRANIAN_INTERFACE=asginl
|
ENV GRANIAN_INTERFACE=asginl
|
||||||
ENV GRANIAN_LOOP=asyncio
|
ENV GRANIAN_LOOP=asyncio
|
||||||
ENV GRANIAN_LOOP=asyncio
|
|
||||||
ENV GRANIAN_LOG_ENABLED=false
|
ENV GRANIAN_LOG_ENABLED=false
|
||||||
|
|
||||||
ENTRYPOINT ["/var/bugis/.venv/bin/python", "-m", "granian", "bugis.asgi:application"]
|
ENTRYPOINT ["/var/bugis/.venv/bin/python", "-m", "granian", "bugis.asgi:application"]
|
||||||
|
@@ -58,3 +58,4 @@ exclude = ["scripts", "docs", "test"]
|
|||||||
strict = true
|
strict = true
|
||||||
|
|
||||||
[tool.setuptools_scm]
|
[tool.setuptools_scm]
|
||||||
|
version_file = "src/bugis/_version.py"
|
@@ -13,7 +13,8 @@ with open(logging_configuration_file, 'r') as input_file:
|
|||||||
configure_logging(conf)
|
configure_logging(conf)
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger('access')
|
||||||
|
log.propagate = False
|
||||||
|
|
||||||
_server = None
|
_server = None
|
||||||
async def application(ctx, receive, send):
|
async def application(ctx, receive, send):
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
from watchdog.events import FileSystemEventHandler, FileSystemEvent, PatternMatchingEventHandler
|
from watchdog.events import FileSystemEventHandler, FileSystemEvent, PatternMatchingEventHandler
|
||||||
from watchdog.observers import Observer
|
from watchdog.observers import Observer
|
||||||
@@ -8,6 +9,7 @@ from asyncio import Queue, AbstractEventLoop, Future, CancelledError, Task
|
|||||||
from typing import Optional, Callable
|
from typing import Optional, Callable
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
|
log : logging.Logger = getLogger(__name__)
|
||||||
|
|
||||||
class Subscription:
|
class Subscription:
|
||||||
_unsubscribe_callback: Callable[['Subscription'], None]
|
_unsubscribe_callback: Callable[['Subscription'], None]
|
||||||
@@ -21,10 +23,12 @@ class Subscription:
|
|||||||
|
|
||||||
def unsubscribe(self) -> None:
|
def unsubscribe(self) -> None:
|
||||||
self._unsubscribe_callback(self)
|
self._unsubscribe_callback(self)
|
||||||
|
log.debug('Deleted subscription %s', id(self))
|
||||||
|
|
||||||
async def wait(self, tout: float) -> bool:
|
async def wait(self, tout: float) -> bool:
|
||||||
handle = self._loop.call_later(tout, lambda: self._event.cancel())
|
handle = self._loop.call_later(tout, lambda: self._event.cancel())
|
||||||
try:
|
try:
|
||||||
|
log.debug('Subscription %s is waiting for an event', id(self))
|
||||||
await self._event
|
await self._event
|
||||||
return True
|
return True
|
||||||
except CancelledError:
|
except CancelledError:
|
||||||
@@ -33,6 +37,7 @@ class Subscription:
|
|||||||
handle.cancel()
|
handle.cancel()
|
||||||
|
|
||||||
def notify(self) -> None:
|
def notify(self) -> None:
|
||||||
|
log.debug('Subscription %s notified', id(self))
|
||||||
self._event.set_result(None)
|
self._event.set_result(None)
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
@@ -85,7 +90,6 @@ def watch(path: Path, queue: Queue, loop: AbstractEventLoop,
|
|||||||
observer.join()
|
observer.join()
|
||||||
loop.call_soon_threadsafe(queue.put_nowait, None)
|
loop.call_soon_threadsafe(queue.put_nowait, None)
|
||||||
|
|
||||||
|
|
||||||
class SubscriptionManager:
|
class SubscriptionManager:
|
||||||
_loop: AbstractEventLoop
|
_loop: AbstractEventLoop
|
||||||
_queue: Queue
|
_queue: Queue
|
||||||
@@ -104,6 +108,7 @@ class SubscriptionManager:
|
|||||||
subscriptions_per_path.remove(subscription)
|
subscriptions_per_path.remove(subscription)
|
||||||
|
|
||||||
result = Subscription(unsubscribe_callback, self._loop)
|
result = Subscription(unsubscribe_callback, self._loop)
|
||||||
|
log.debug('Created subscription %s to path %s', id(result), path)
|
||||||
subscriptions_per_path.add(result)
|
subscriptions_per_path.add(result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@@ -4,8 +4,13 @@ handlers:
|
|||||||
console:
|
console:
|
||||||
class : logging.StreamHandler
|
class : logging.StreamHandler
|
||||||
formatter: default
|
formatter: default
|
||||||
|
level : DEBUG
|
||||||
|
stream : ext://sys.stderr
|
||||||
|
access:
|
||||||
|
class : logging.StreamHandler
|
||||||
|
formatter: request
|
||||||
level : INFO
|
level : INFO
|
||||||
stream : ext://sys.stdout
|
stream : ext://sys.stderr
|
||||||
formatters:
|
formatters:
|
||||||
brief:
|
brief:
|
||||||
format: '%(message)s'
|
format: '%(message)s'
|
||||||
@@ -13,7 +18,14 @@ formatters:
|
|||||||
format: '{asctime} [{levelname}] ({processName:s}/{threadName:s}) - {name} - {message}'
|
format: '{asctime} [{levelname}] ({processName:s}/{threadName:s}) - {name} - {message}'
|
||||||
style: '{'
|
style: '{'
|
||||||
datefmt: '%Y-%m-%d %H:%M:%S'
|
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:
|
loggers:
|
||||||
root:
|
root:
|
||||||
handlers: [console]
|
handlers: [console]
|
||||||
|
level: DEBUG
|
||||||
|
access:
|
||||||
|
handlers: [access]
|
||||||
level: INFO
|
level: INFO
|
@@ -2,7 +2,7 @@ import sys
|
|||||||
from os.path import dirname, join, relpath
|
from os.path import dirname, join, relpath
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
from aiofiles import open as async_open
|
||||||
import markdown
|
import markdown
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -20,21 +20,21 @@ STATIC_CACHE: dict[str, tuple[str, float]] = {}
|
|||||||
MARDOWN_EXTENSIONS = ['extra', 'smarty', 'tables', 'codehilite']
|
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
|
global STATIC_CACHE
|
||||||
if path not in STATIC_CACHE:
|
if path not in STATIC_CACHE:
|
||||||
with open(join(dirname(__file__), 'static') + path, 'r') as static_file:
|
async with async_open(join(dirname(__file__), 'static') + path, 'r') as static_file:
|
||||||
STATIC_CACHE[path] = (static_file.read(), time())
|
STATIC_CACHE[path] = (await static_file.read(), time())
|
||||||
return STATIC_CACHE[path]
|
return STATIC_CACHE[path]
|
||||||
|
|
||||||
|
|
||||||
def compile_html(url_path,
|
async def compile_html(url_path,
|
||||||
mdfile: 'StrOrBytesPath',
|
mdfile: 'StrOrBytesPath',
|
||||||
prefix: Optional['StrOrBytesPath'] = None,
|
prefix: Optional['StrOrBytesPath'] = None,
|
||||||
extensions: Optional[list[str]] = None,
|
extensions: Optional[list[str]] = None,
|
||||||
raw: bool = False) -> str:
|
raw: bool = False) -> str:
|
||||||
with mdfile and open(mdfile, 'r') or sys.stdin as instream:
|
async with mdfile and async_open(mdfile, 'r') or sys.stdin as instream:
|
||||||
html = markdown.markdown(instream.read(), extensions=extensions, output_format='html')
|
html = markdown.markdown(await instream.read(), extensions=extensions, output_format='html')
|
||||||
if raw:
|
if raw:
|
||||||
doc = html
|
doc = html
|
||||||
else:
|
else:
|
||||||
@@ -44,5 +44,5 @@ def compile_html(url_path,
|
|||||||
css = f'<link rel="icon" type="image/x-icon" href="{prefix}/markdown.svg">'
|
css = f'<link rel="icon" type="image/x-icon" href="{prefix}/markdown.svg">'
|
||||||
for css_file in ('github-markdown.css', 'pygment.css', 'custom.css'):
|
for css_file in ('github-markdown.css', 'pygment.css', 'custom.css'):
|
||||||
css += f' <link rel="stylesheet" href="{prefix}/{css_file}">'
|
css += f' <link rel="stylesheet" href="{prefix}/{css_file}">'
|
||||||
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
|
return doc
|
||||||
|
@@ -70,12 +70,12 @@ class Server:
|
|||||||
url_path: 'StrOrBytesPath' = normpath(join('/', relative_path))
|
url_path: 'StrOrBytesPath' = normpath(join('/', relative_path))
|
||||||
path: 'StrOrBytesPath' = join(self.root_dir, relative_path)
|
path: 'StrOrBytesPath' = join(self.root_dir, relative_path)
|
||||||
if url_path in STATIC_RESOURCES:
|
if url_path in STATIC_RESOURCES:
|
||||||
content, mtime = load_from_cache(url_path)
|
content, mtime = await load_from_cache(url_path)
|
||||||
content = content.encode()
|
content = content.encode()
|
||||||
etag, digest = await self.compute_etag_and_digest(
|
etag, digest = await self.compute_etag_and_digest(
|
||||||
etag,
|
etag,
|
||||||
url_path,
|
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)
|
lambda: completed_future(mtime)
|
||||||
)
|
)
|
||||||
if etag and etag == digest:
|
if etag and etag == digest:
|
||||||
@@ -267,11 +267,11 @@ class Server:
|
|||||||
raw: bool,
|
raw: bool,
|
||||||
digest: str,
|
digest: str,
|
||||||
send) -> list[bytes]:
|
send) -> list[bytes]:
|
||||||
body = compile_html(url_path,
|
body = (await compile_html(url_path,
|
||||||
path,
|
path,
|
||||||
self.prefix,
|
self.prefix,
|
||||||
MARDOWN_EXTENSIONS,
|
MARDOWN_EXTENSIONS,
|
||||||
raw=raw).encode()
|
raw=raw)).encode()
|
||||||
await send({
|
await send({
|
||||||
'type': 'http.response.start',
|
'type': 'http.response.start',
|
||||||
'status': 200,
|
'status': 200,
|
||||||
|
Reference in New Issue
Block a user