3 Commits
Author SHA1 Message Date
woggioni df5906fd3c tmp 2026-06-30 00:23:32 +08:00
woggioni 77362fd05c updated dependencies 2026-06-30 00:23:15 +08:00
woggioni f158698380 temporary commit 2025-04-02 17:21:30 +08:00
11 changed files with 319 additions and 207 deletions
+38 -7
View File
@@ -1,12 +1,13 @@
from bugis.core import BugisApp, HttpContext from bugis.core import BugisApp, HttpContext, HttpMethod
from typing import List
class Hello(BugisApp): class Hello(BugisApp):
pass
async def handle_request(self, ctx: HttpContext) -> None: # async def handle_request(self, ctx: HttpContext) -> None:
async for chunk in ctx.request_body: # async for chunk in ctx.request_body:
print(chunk) # print(chunk.decode())
await ctx.send_str(200, 'Hello World') # await ctx.send_str(200, 'Hello World')
app = BugisApp() app = BugisApp()
@@ -16,5 +17,35 @@ app = BugisApp()
@app.GET('/hello2') @app.GET('/hello2')
async def handle_request(ctx: HttpContext) -> None: async def handle_request(ctx: HttpContext) -> None:
async for chunk in ctx.request_body: async for chunk in ctx.request_body:
print(chunk) print(chunk.decode())
await ctx.send_str(200, 'Hello World') await ctx.send_str(200, 'Hello World')
@app.GET('/hello/alice')
async def hello_alice(ctx: HttpContext) -> None:
async for _ in ctx.request_body:
pass
await ctx.send_str(200, 'Hello Alice 2')
@app.GET('/hello/*')
async def hello_alice(ctx: HttpContext, args: List[str]) -> None:
async for _ in ctx.request_body:
pass
await ctx.send_str(200, f'Hello {args[0]}')
@app.GET('/hello/*/kennedy')
async def hello_alice(ctx: HttpContext, args: List[str]) -> None:
async for _ in ctx.request_body:
pass
await ctx.send_str(200, f'Hello {args[0]} kennedy')
@app.GET('/hello/bob/marley')
async def hello_alice(ctx: HttpContext) -> None:
async for _ in ctx.request_body:
pass
await ctx.send_str(200, f'Hello bob Marley')
@app.route('/hello/*', HttpMethod.PUT, recursive=True)
async def handle_request(ctx: HttpContext, _: List[str]) -> None:
async for chunk in ctx.request_body:
print(chunk.decode())
await ctx.send_str(200, 'Message received')
+75 -59
View File
@@ -1,102 +1,127 @@
# #
# 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 # pip-compile --allow-unsafe --extra=dev --extra=rsgi --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 anyio==4.14.1
# via bugis_core (pyproject.toml) # 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_core (pyproject.toml) # via bugis_core (pyproject.toml)
certifi==2024.8.30 certifi==2026.6.17
# via requests # via
cffi==1.17.1 # httpcore
# httpx
# requests
cffi==2.0.0
# via cryptography # via cryptography
charset-normalizer==3.4.0 charset-normalizer==3.4.7
# via requests # via requests
cryptography==43.0.3 click==8.4.2
# via granian
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
idna==3.10 granian==2.7.7
# via requests # via bugis_core (pyproject.toml)
importlib-metadata==8.5.0 h11==0.16.0
# via httpcore
httpcore==1.0.9
# via httpx
httpx==0.28.1
# via bugis_core (pyproject.toml)
id==1.6.1
# via twine # via twine
idna==3.18
# via
# anyio
# httpx
# requests
ipdb==0.13.13 ipdb==0.13.13
# via bugis_core (pyproject.toml) # via bugis_core (pyproject.toml)
ipython==8.29.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.5.0 keyring==25.7.0
# via twine # via twine
markdown-it-py==3.0.0 librt==0.11.0
# via mypy
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_core (pyproject.toml) # via bugis_core (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.5 pwo==0.1.2
# via bugis_core (pyproject.toml) # via bugis_core (pyproject.toml)
pycparser==2.22 pycparser==3.0
# via cffi # via cffi
pygments==2.18.0 pygments==2.20.0
# via # via
# ipython # ipython
# ipython-pygments-lexers
# readme-renderer # readme-renderer
# rich # rich
pyproject-hooks==1.2.0 pyproject-hooks==1.2.0
# via build # via build
pyyaml==6.0.2 readme-renderer==45.0
# via bugis_core (pyproject.toml)
readme-renderer==44.0
# via twine # via twine
requests==2.32.3 requests==2.34.2
# via # via
# requests-toolbelt # requests-toolbelt
# twine # twine
@@ -104,35 +129,26 @@ 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
sortedcontainers==2.4.0
# via bugis_core (pyproject.toml)
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_core (pyproject.toml) # via bugis_core (pyproject.toml)
types-pyyaml==6.0.12.20240917 typing-extensions==4.15.0
# via bugis_core (pyproject.toml)
typing-extensions==4.12.2
# via # via
# mypy # mypy
# pwo # pwo
urllib3==2.2.3 urllib3==2.7.0
# via # via
# id
# requests # requests
# twine # twine
watchdog==5.0.3 wcwidth==0.8.1
# via bugis_core (pyproject.toml)
wcwidth==0.2.13
# via prompt-toolkit # via prompt-toolkit
zipp==3.20.2
# via importlib-metadata
+7 -11
View File
@@ -1,21 +1,17 @@
# #
# 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 # pip-compile --allow-unsafe --extra=rsgi --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 click==8.4.2
# via granian
granian==2.7.7
# via bugis_core (pyproject.toml) # via bugis_core (pyproject.toml)
pwo==0.0.5 pwo==0.1.2
# via bugis_core (pyproject.toml) # via bugis_core (pyproject.toml)
pyyaml==6.0.2 typing-extensions==4.15.0
# via bugis_core (pyproject.toml)
sortedcontainers==2.4.0
# via bugis_core (pyproject.toml)
typing-extensions==4.12.2
# via pwo # via pwo
watchdog==5.0.3
# via bugis_core (pyproject.toml)
+1 -1
View File
@@ -52,7 +52,7 @@ class RsgiContext(HttpContext):
self.server = (Maybe.of(scope.server.split(':')) self.server = (Maybe.of(scope.server.split(':'))
.map(lambda it: (it[0], int(it[1]))) .map(lambda it: (it[0], int(it[1])))
.or_else_throw(RuntimeError)) .or_else_throw(RuntimeError))
self.request_body = aiter(protocol) self.request_body = protocol
self.protocol = protocol self.protocol = protocol
# @staticmethod # @staticmethod
+21 -10
View File
@@ -3,7 +3,7 @@ import json
import httpx import httpx
from pwo import async_test from pwo import async_test
from bugis.core import BugisApp, HttpContext, HttpMethod from bugis.core import BugisApp, HttpContext, HttpMethod
from typing import Sequence from typing import Sequence, List
class AsgiTest(unittest.TestCase): class AsgiTest(unittest.TestCase):
@@ -15,6 +15,7 @@ class AsgiTest(unittest.TestCase):
@self.app.GET('/hello') @self.app.GET('/hello')
@self.app.GET('/hello2') @self.app.GET('/hello2')
@self.app.route('/hello3') @self.app.route('/hello3')
@self.app.GET('/hello/*')
async def handle_request(ctx: HttpContext) -> None: async def handle_request(ctx: HttpContext) -> None:
async for chunk in ctx.request_body: async for chunk in ctx.request_body:
print(chunk) print(chunk)
@@ -53,34 +54,44 @@ class AsgiTest(unittest.TestCase):
'employee_id': employee_id 'employee_id': employee_id
})) }))
@self.app.PUT('/hello/*', recursive=True)
async def handle_request(ctx: HttpContext, _: List[str]) -> None:
await ctx.stream_body(200, (chunk async for chunk in ctx.request_body))
@async_test @async_test
async def test_hello(self): async def test_hello(self):
transport = httpx.ASGITransport(app=self.app) transport = httpx.ASGITransport(app=self.app)
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:80") as client: async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:80") as client:
r = await client.get("/hello") r = await client.get("/hello")
self.assertEqual(r.status_code, 200) self.assertEqual(200, r.status_code)
self.assertEqual(r.text, "Hello World!") self.assertEqual(r.text, "Hello World!")
r = await client.get("/hello2") r = await client.get("/hello2")
self.assertEqual(r.status_code, 200) self.assertEqual(200, r.status_code)
self.assertEqual(r.text, "Hello World!") self.assertEqual(r.text, "Hello World!")
r = await client.post("/hello3") r = await client.post("/hello3")
self.assertEqual(r.status_code, 200) self.assertEqual(200, r.status_code)
self.assertEqual(r.text, "Hello World!") self.assertEqual(r.text, "Hello World!")
r = await client.get("/hello4") r = await client.get("/hello4")
self.assertEqual(r.status_code, 404) self.assertEqual(404, r.status_code)
self.assertTrue(len(r.text) == 0) self.assertTrue(len(r.text) == 0)
body = {'name': 'John', 'surname': 'Smith'}
r = await client.put("/hello/foo/bar", json=body)
self.assertEqual(200, r.status_code)
ans = json.loads(r.text)
self.assertTrue(body, ans)
@async_test @async_test
async def test_foo(self): async def test_foo(self):
transport = httpx.ASGITransport(app=self.app) transport = httpx.ASGITransport(app=self.app)
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:80") as client: async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:80") as client:
r = await client.put("/foo/fizz/baz") r = await client.put("/foo/fizz/baz")
self.assertEqual(r.status_code, 200) self.assertEqual(200, r.status_code)
response = json.loads(r.text) response = json.loads(r.text)
self.assertEqual(['fizz', 'baz'], response) self.assertEqual(['fizz', 'baz'], response)
@@ -90,7 +101,7 @@ class AsgiTest(unittest.TestCase):
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:80") as client: async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:80") as client:
r = await client.put("/foo/bar/baz") r = await client.put("/foo/bar/baz")
self.assertEqual(r.status_code, 200) self.assertEqual(200, r.status_code)
self.assertEqual('/foo/bar/baz', r.text) self.assertEqual('/foo/bar/baz', r.text)
@async_test @async_test
@@ -99,7 +110,7 @@ class AsgiTest(unittest.TestCase):
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:80") as client: async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:80") as client:
r = await client.get("/employee/101325") r = await client.get("/employee/101325")
self.assertEqual(r.status_code, 200) self.assertEqual(200, r.status_code)
self.assertEqual(r.text, '101325') self.assertEqual(r.text, '101325')
@async_test @async_test
@@ -109,7 +120,7 @@ class AsgiTest(unittest.TestCase):
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:80") as client: async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:80") as client:
x = 30 x = 30
r = await client.get(f"/square/{x}") r = await client.get(f"/square/{x}")
self.assertEqual(r.status_code, 200) self.assertEqual(200, r.status_code)
self.assertEqual(r.text, str(x * x)) self.assertEqual(r.text, str(x * x))
@async_test @async_test
@@ -118,7 +129,7 @@ class AsgiTest(unittest.TestCase):
async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:80") as client: async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:80") as client:
r = await client.get("department/189350/employee/101325") r = await client.get("department/189350/employee/101325")
self.assertEqual(r.status_code, 200) self.assertEqual(200, r.status_code)
response = json.loads(r.text) response = json.loads(r.text)
self.assertEqual({ self.assertEqual({
'department_id': 189350, 'department_id': 189350,
+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)
+37 -9
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
pass
@abstractmethod def get(self, key: bytes, file: PurePath) -> Optional[Tuple[str, bytes]]:
def put(self, key: bytes, value: bytes): if self.filter(file):
pass return self.load(key, file)
else:
return self._rendering_manager.render(file)
def __contains__(self, key: bytes): def filter(self, file: PurePath) -> bool:
return self.get(key) is not None 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
class InMemoryCache(Cache):
_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
+33 -17
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,24 +73,33 @@ 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:
mime_type = (Maybe.of(guess_type(resource.name)) cache_result = cache.get(b64decode(current_etag), resource)
.map(lambda it: it[0]) if cache_result is None:
.or_else('application/octet-stream')) mime_type = (Maybe.of(guess_type(resource.name))
return await context.send_file(200, resource, { .map(lambda it: it[0])
'content-type': mime_type or 'application/octet-stream', .or_else('application/octet-stream'))
'etag': 'W/' + current_etag, return await context.send_file(200, resource, {
'cache-control': 'no-cache', 'content-type': mime_type or 'application/octet-stream',
}) 'etag': 'W/' + current_etag,
'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'