2 Commits
Author SHA1 Message Date
woggioni 0ef10b9b8f updated dependencies 2026-06-27 14:09:45 +08:00
woggioni f158698380 temporary commit 2025-04-02 17:21:30 +08:00
6 changed files with 176 additions and 118 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ services:
cpus: "4" cpus: "4"
memory: 1G memory: 1G
nginx: nginx:
image: gitea.woggioni.net/woggioni/nginx:v1.27.2 image: gitea.woggioni.net/woggioni/nginx:v1.31.2
# container_name: nginx # container_name: nginx
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
+70 -68
View File
@@ -1,131 +1,142 @@
# #
# This file is autogenerated by pip-compile with Python 3.12 # This file is autogenerated by pip-compile with Python 3.14
# by the following command: # by the following command:
# #
# pip-compile --extra=dev --output-file=requirements-dev.txt pyproject.toml # pip-compile --allow-unsafe --extra=dev --output-file=requirements-dev.txt pyproject.toml
# #
--index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple --index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple
--extra-index-url https://pypi.org/simple --extra-index-url https://pypi.org/simple
aiofiles==24.1.0 aiofiles==25.1.0
# via bugis (pyproject.toml) # via bugis (pyproject.toml)
anyio==4.6.2.post1 anyio==4.14.1
# via httpx # via httpx
asttokens==2.4.1 ast-serialize==0.5.0
# via mypy
asttokens==3.0.1
# via stack-data # via stack-data
build==1.2.2.post1 build==1.5.0
# via bugis (pyproject.toml) # via bugis (pyproject.toml)
certifi==2024.8.30 certifi==2026.6.17
# via # via
# httpcore # httpcore
# httpx # httpx
# requests # requests
cffi==1.17.1 cffi==2.0.0
# via cryptography # via cryptography
charset-normalizer==3.4.0 charset-normalizer==3.4.7
# via requests # via requests
click==8.1.7 click==8.4.2
# via granian # via granian
cryptography==43.0.3 cryptography==49.0.0
# via secretstorage # via secretstorage
decorator==5.1.1 decorator==5.3.1
# via # via
# ipdb # ipdb
# ipython # ipython
docutils==0.21.2 docutils==0.23
# via readme-renderer # via readme-renderer
executing==2.1.0 executing==2.2.1
# via stack-data # via stack-data
granian==1.6.1 granian==2.7.7
# via bugis (pyproject.toml) # via bugis (pyproject.toml)
h11==0.14.0 h11==0.16.0
# via httpcore # via httpcore
h2==4.1.0 h2==4.3.0
# via httpx # via httpx
hpack==4.0.0 hpack==4.2.0
# via h2 # via h2
httpcore==1.0.6 httpcore==1.0.9
# via httpx # via httpx
httpx[http2]==0.27.2 httpx[http2]==0.28.1
# via bugis (pyproject.toml) # via bugis (pyproject.toml)
hyperframe==6.0.1 hyperframe==6.1.0
# via h2 # via h2
idna==3.10 id==1.6.1
# via twine
idna==3.18
# via # via
# anyio # anyio
# httpx # httpx
# requests # requests
importlib-metadata==8.5.0
# via twine
ipdb==0.13.13 ipdb==0.13.13
# via bugis (pyproject.toml) # via bugis (pyproject.toml)
ipython==8.28.0 ipython==9.15.0
# via ipdb # via ipdb
ipython-pygments-lexers==1.1.1
# via ipython
jaraco-classes==3.4.0 jaraco-classes==3.4.0
# via keyring # via keyring
jaraco-context==6.0.1 jaraco-context==6.1.2
# via keyring # via keyring
jaraco-functools==4.1.0 jaraco-functools==4.5.0
# via keyring # via keyring
jedi==0.19.1 jedi==0.20.0
# via ipython # via ipython
jeepney==0.8.0 jeepney==0.9.0
# via # via
# keyring # keyring
# secretstorage # secretstorage
keyring==25.4.1 keyring==25.7.0
# via twine # via twine
markdown==3.7 librt==0.11.0
# via mypy
markdown==3.10.2
# via bugis (pyproject.toml) # via bugis (pyproject.toml)
markdown-it-py==3.0.0 markdown-it-py==4.2.0
# via rich # via rich
matplotlib-inline==0.1.7 matplotlib-inline==0.2.2
# via ipython # via ipython
mdurl==0.1.2 mdurl==0.1.2
# via markdown-it-py # via markdown-it-py
more-itertools==10.5.0 more-itertools==11.1.0
# via # via
# jaraco-classes # jaraco-classes
# jaraco-functools # jaraco-functools
mypy==1.13.0 mypy==2.1.0
# via bugis (pyproject.toml) # via bugis (pyproject.toml)
mypy-extensions==1.0.0 mypy-extensions==1.1.0
# via mypy # via mypy
nh3==0.2.18 nh3==0.3.6
# via readme-renderer # via readme-renderer
packaging==24.1 packaging==26.2
# via build # via
parso==0.8.4 # build
# twine
parso==0.8.7
# via jedi # via jedi
pathspec==1.1.1
# via mypy
pexpect==4.9.0 pexpect==4.9.0
# via ipython # via ipython
pkginfo==1.10.0 prompt-toolkit==3.0.52
# via twine # via ipython
prompt-toolkit==3.0.48 psutil==7.2.2
# via ipython # via ipython
ptyprocess==0.7.0 ptyprocess==0.7.0
# via pexpect # via pexpect
pure-eval==0.2.3 pure-eval==0.2.3
# via stack-data # via stack-data
pwo==0.0.4 pwo==0.1.2
# via bugis (pyproject.toml) # via bugis (pyproject.toml)
pycparser==2.22 pycparser==3.0
# via cffi # via cffi
pygments==2.18.0 pygments==2.20.0
# via # via
# bugis (pyproject.toml) # bugis (pyproject.toml)
# ipython # ipython
# ipython-pygments-lexers
# readme-renderer # readme-renderer
# rich # rich
pygraphviz==1.14 pygraphviz==2.0
# via bugis (pyproject.toml) # via bugis (pyproject.toml)
pyproject-hooks==1.2.0 pyproject-hooks==1.2.0
# via build # via build
pyyaml==6.0.2 pyyaml==6.0.3
# via bugis (pyproject.toml) # via bugis (pyproject.toml)
readme-renderer==44.0 readme-renderer==45.0
# via twine # via twine
requests==2.32.3 requests==2.34.2
# via # via
# requests-toolbelt # requests-toolbelt
# twine # twine
@@ -133,37 +144,28 @@ requests-toolbelt==1.0.0
# via twine # via twine
rfc3986==2.0.0 rfc3986==2.0.0
# via twine # via twine
rich==13.9.3 rich==15.0.0
# via twine # via twine
secretstorage==3.3.3 secretstorage==3.5.0
# via keyring # via keyring
six==1.16.0
# via asttokens
sniffio==1.3.1
# via
# anyio
# httpx
stack-data==0.6.3 stack-data==0.6.3
# via ipython # via ipython
traitlets==5.14.3 traitlets==5.15.1
# via # via
# ipython # ipython
# matplotlib-inline # matplotlib-inline
twine==5.1.1 twine==6.2.0
# via bugis (pyproject.toml) # via bugis (pyproject.toml)
typing-extensions==4.12.2 typing-extensions==4.15.0
# via # via
# mypy # mypy
# pwo # pwo
urllib3==2.2.3 urllib3==2.7.0
# via # via
# id
# requests # requests
# twine # twine
uvloop==0.21.0 watchdog==6.0.0
# via granian
watchdog==5.0.3
# via bugis (pyproject.toml) # via bugis (pyproject.toml)
wcwidth==0.2.13 wcwidth==0.8.1
# via prompt-toolkit # via prompt-toolkit
zipp==3.20.2
# via importlib-metadata
+19 -23
View File
@@ -1,51 +1,47 @@
# #
# This file is autogenerated by pip-compile with Python 3.12 # This file is autogenerated by pip-compile with Python 3.14
# by the following command: # by the following command:
# #
# pip-compile --output-file=requirements.txt pyproject.toml # pip-compile --allow-unsafe --output-file=requirements.txt pyproject.toml
# #
--index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple --index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple
--extra-index-url https://pypi.org/simple --extra-index-url https://pypi.org/simple
aiofiles==24.1.0 aiofiles==25.1.0
# via bugis (pyproject.toml) # via bugis (pyproject.toml)
anyio==4.6.2.post1 anyio==4.14.1
# via httpx # via httpx
certifi==2024.8.30 certifi==2026.6.17
# via # via
# httpcore # httpcore
# httpx # httpx
h11==0.14.0 h11==0.16.0
# via httpcore # via httpcore
h2==4.1.0 h2==4.3.0
# via httpx # via httpx
hpack==4.0.0 hpack==4.2.0
# via h2 # via h2
httpcore==1.0.6 httpcore==1.0.9
# via httpx # via httpx
httpx[http2]==0.27.2 httpx[http2]==0.28.1
# via bugis (pyproject.toml) # via bugis (pyproject.toml)
hyperframe==6.0.1 hyperframe==6.1.0
# via h2 # via h2
idna==3.10 idna==3.18
# via # via
# anyio # anyio
# httpx # httpx
markdown==3.7 markdown==3.10.2
# via bugis (pyproject.toml) # via bugis (pyproject.toml)
pwo==0.0.4 pwo==0.1.2
# via bugis (pyproject.toml) # via bugis (pyproject.toml)
pygments==2.18.0 pygments==2.20.0
# via bugis (pyproject.toml) # via bugis (pyproject.toml)
pygraphviz==1.14 pygraphviz==2.0
# via bugis (pyproject.toml) # via bugis (pyproject.toml)
pyyaml==6.0.2 pyyaml==6.0.3
# via bugis (pyproject.toml) # via bugis (pyproject.toml)
sniffio==1.3.1 typing-extensions==4.15.0
# via
# anyio
# httpx
typing-extensions==4.12.2
# via pwo # via pwo
watchdog==5.0.3 watchdog==6.0.0
# via bugis (pyproject.toml) # via bugis (pyproject.toml)
+36 -8
View File
@@ -1,16 +1,44 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Optional from typing import Optional, Tuple, Callable
from functools import lru_cache
from pathlib import PurePath
from .renderer import RenderingManager
class Cache(ABC): class Cache(ABC):
_rendering_manager: RenderingManager
@abstractmethod def __init__(self, rendering_manager: RenderingManager):
def get(self, key: bytes) -> Optional[bytes]: self._rendering_manager = rendering_manager
def get(self, key: bytes, file: PurePath) -> Optional[Tuple[str, bytes]]:
if self.filter(file):
return self.load(key, file)
else:
return self._rendering_manager.render(file)
def filter(self, file: PurePath) -> bool:
return file.suffix.lower() in {'md', 'puml', 'dot', 'texi', 'texinfo', 'txi'}
def load(self, key: bytes, file: PurePath) -> Optional[Tuple[str, bytes]]:
return self._rendering_manager.render(file)
class NoCache(Cache):
pass pass
@abstractmethod
def put(self, key: bytes, value: bytes):
pass
def __contains__(self, key: bytes): class InMemoryCache(Cache):
return self.get(key) is not None _cached_loader: Callable[[bytes, PurePath], Tuple[str, bytes]]
def __init__(self, rendering_manager: RenderingManager, max_size: int = 1024):
super().__init__(rendering_manager)
@lru_cache(maxsize=max_size)
def cached_loader(key: bytes, file: PurePath) -> Tuple[str, bytes]:
return super().load(key, file)
self._cached_loader = cached_loader
def load(self, key: bytes, file: PurePath) -> Optional[Tuple[str, bytes]]:
return self._cached_loader(key, file)
+16
View File
@@ -0,0 +1,16 @@
from pathlib import PurePath
from typing import Callable, Mapping, Optional, Tuple
from pwo import Maybe
class RenderingManager:
_renderers: Mapping[str, Callable[[PurePath], Tuple[str, bytes]]]
def __init__(self, renderers: Mapping[str, Callable[[PurePath], Tuple[str, bytes]]]):
self._renderers = renderers
def render(self, file: PurePath) -> Optional[Tuple[str, bytes]]:
return Maybe.of_nullable(self._renderers.get(file.suffix.lower())).map(lambda it: it(file)).or_none()
# def register(self, suffix: str, renderer: Callable[[PurePath], Tuple[str, bytes]]) -> None:
# self._renderers[suffix.lower()] = renderer
+21 -5
View File
@@ -1,13 +1,15 @@
from base64 import b64encode from base64 import b64encode, b64decode
from hashlib import md5 from hashlib import md5
from mimetypes import guess_type from mimetypes import guess_type
from pathlib import Path, PurePath from pathlib import Path, PurePath
from typing import TYPE_CHECKING, Optional, Callable, Awaitable, AsyncGenerator, Any, Unpack from typing import TYPE_CHECKING, Optional, Callable, Awaitable, AsyncGenerator, Any, Unpack, Mapping, Tuple
from aiofiles.os import listdir from aiofiles.os import listdir
from aiofiles.ospath import isdir, isfile from aiofiles.ospath import isdir, isfile
from bugis.core import HttpContext, BugisApp from bugis.core import HttpContext, BugisApp
from pwo import Maybe from pwo import Maybe
from .cache import Cache, InMemoryCache
from .renderer import RenderingManager
if TYPE_CHECKING: if TYPE_CHECKING:
from _typeshed import StrOrBytesPath from _typeshed import StrOrBytesPath
@@ -31,8 +33,13 @@ def static_resources(app: BugisApp,
path: str, path: str,
root: 'StrOrBytesPath', root: 'StrOrBytesPath',
favicon: PurePath = None, favicon: PurePath = None,
file_filter: Callable[[PurePath], Awaitable[bool]] = None file_filter: Callable[[PurePath], Awaitable[bool]] = None,
renderers: Mapping[str, Tuple[str, bytes]] = None,
cache_ctor: Callable[[RenderingManager], Cache] = lambda rm: InMemoryCache(rm)
): ):
renderer = RenderingManager(renderers or {})
cache = cache_ctor(renderer)
async def no_filter(_: PurePath): async def no_filter(_: PurePath):
return True return True
@@ -66,16 +73,18 @@ def static_resources(app: BugisApp,
if await isfile(resource): if await isfile(resource):
e_tag_header = ((Maybe.of_nullable(context.headers.get('if-none-match')) proposed_etag = ((Maybe.of_nullable(context.headers.get('if-none-match'))
.filter(lambda it: len(it) > 0) .filter(lambda it: len(it) > 0)
.map(lambda it: it[-1]) .map(lambda it: it[-1])
.map(parse_etag)) .map(parse_etag))
.or_none()) .or_none())
current_etag = compute_etag(resource) current_etag = compute_etag(resource)
if e_tag_header == current_etag: if proposed_etag == current_etag:
return await context.send_empty(304) return await context.send_empty(304)
else: else:
cache_result = cache.get(b64decode(current_etag), resource)
if cache_result is None:
mime_type = (Maybe.of(guess_type(resource.name)) mime_type = (Maybe.of(guess_type(resource.name))
.map(lambda it: it[0]) .map(lambda it: it[0])
.or_else('application/octet-stream')) .or_else('application/octet-stream'))
@@ -84,6 +93,13 @@ def static_resources(app: BugisApp,
'etag': 'W/' + current_etag, 'etag': 'W/' + current_etag,
'cache-control': 'no-cache', 'cache-control': 'no-cache',
}) })
else:
content_type, body = cache_result
await context.send_bytes(200, body, {
'content-type': content_type,
'etag': 'W/' + current_etag,
'cache-control': 'no-cache',
})
elif isdir(resource): elif isdir(resource):
headers = { headers = {
'content-type': 'text/html' 'content-type': 'text/html'