Skip to content

Commit

Permalink
feat: auto mount directory of local favicon file
Browse files Browse the repository at this point in the history
  • Loading branch information
waketzheng committed Dec 14, 2023
1 parent 4074b2c commit a9b5bbf
Show file tree
Hide file tree
Showing 15 changed files with 181 additions and 13 deletions.
52 changes: 42 additions & 10 deletions fastapi_cdn_host/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def __init__(self, app=None, docs_cdn_host=None, favicon_url=None) -> None:

@staticmethod
def run_async(async_func, *args) -> Any:
"""Run async function in worker thread and get the result of it"""
result = [None]

async def runner():
Expand All @@ -110,13 +111,19 @@ def run(self) -> AssetUrl:
static_builder.static_root = self.docs_cdn_host
if urls := static_builder.find():
return urls
return self.run_async(self.sniff_the_fastest, self.favicon_url)
if (favicon := self.favicon_url) is not None:
favicon = self.mount_local_favicon(favicon)
return self.run_async(self.sniff_the_fastest, favicon)

@staticmethod
def fill_root_path(urls, root):
if root:
for attr in ("js", "css", "redoc", "favicon"):
if (v := getattr(urls, attr)) and not v.startswith(root):
if (
(v := getattr(urls, attr))
and v.startswith("/")
and not v.startswith(root)
):
setattr(urls, attr, root + v)
return urls

Expand Down Expand Up @@ -156,6 +163,20 @@ async def sniff_the_fastest(cls, favicon_url=None) -> AssetUrl:
logger.info(f"Select cdn: {fast_host[0]} to serve swagger css/js")
return AssetUrl(css=css, js=js, redoc=redoc, favicon=favicon_url)

def mount_local_favicon(self, favicon_url) -> Union[str, None]:
if favicon_url is not None and favicon_url.startswith("/"):
filename = favicon_url.lstrip("/")
favicon_file = Path(filename)
if favicon_file.exists() and favicon_file.parent.is_dir():
static_root = favicon_file.parents[-2]
uri_path = StaticBuilder.auto_mount_static(
self.app, static_root, "/" + static_root.name
)
favicon_url = StaticBuilder.file_to_uri(
favicon_file, static_root, uri_path
)
return favicon_url


class DocsBuilder:
def __init__(self, index: int) -> None:
Expand Down Expand Up @@ -214,14 +235,16 @@ def __init__(
self.favicon_url = favicon_url

def find(self):
return self.local_file(self.app, self.static_root, self.favicon_url)
return self.detect_local_file(self.app, self.static_root, self.favicon_url)

def _maybe(
self, static_root: Path, mount=None, app=None, favicon=None
) -> Optional[AssetUrl]:
if gs := list(static_root.rglob("swagger-ui*.css")):
logger.info(f"Using local files in {static_root} to serve docs assets.")
return self._next_it(gs, mount, app, static_root, favicon)
return self._generate_asset_urls_from_local_files(
gs, mount, app, static_root, favicon
)
return None

@staticmethod
Expand All @@ -234,16 +257,25 @@ def get_latest_one(gs: List[Path]) -> Path:
def file_to_uri(p: Path, static_root: Path, uri_path: str) -> str:
return uri_path.rstrip("/") + "/" + p.relative_to(static_root).as_posix()

def _next_it(
@staticmethod
def auto_mount_static(
app: FastAPI, static_root: Union[Path, str], uri_path=None
) -> str:
if uri_path is None:
uri_path = "/static"
if all(getattr(r, "path", "") != uri_path for r in app.routes):
name = uri_path.strip("/")
app.mount(uri_path, StaticFiles(directory=static_root), name=name)
logger.info(f"Auto mount static files to {uri_path} from {static_root}")
return uri_path

def _generate_asset_urls_from_local_files(
self, gs, mount=None, app=None, static_root=None, favicon=None
) -> AssetUrl:
if mount:
uri_path = mount.path
else:
uri_path = "/static"
if all(r.path != uri_path for r in app.routes):
app.mount(uri_path, StaticFiles(directory=static_root), name="static")
logger.info(f"Auto mount static files to {uri_path} from {static_root}")
uri_path = self.auto_mount_static(app, static_root)
css_file = self.get_latest_one(gs)
if _js := list(static_root.rglob("swagger-ui*.js")):
js_file = self.get_latest_one(_js)
Expand All @@ -269,7 +301,7 @@ def _next_it(
favicon = self.file_to_uri(favicon_file, static_root, uri_path)
return AssetUrl(css=css, js=js, redoc=redoc, favicon=favicon)

def local_file(
def detect_local_file(
self,
app,
static_root: Union[Path, None] = None,
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,5 @@ check_untyped_defs = true
branch = true
parallel=true
source = ["fastapi_cdn_host"]
[tool.coverage.report]
omit = ["tests/*"]
3 changes: 3 additions & 0 deletions scripts/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@ cd ../custom_static_root && coverage run -m pytest test_*.py
cd ../static_with_favicon && coverage run -m pytest test_*.py
cd ../defined_root_path && coverage run -m pytest test_*.py
cd ../http_race && coverage run -m pytest test_*.py
cd ../root_path_without_static && coverage run -m pytest test_*.py
cd ../static_favicon_without_swagger_ui && coverage run -m pytest test_*.py

cd ../.. && coverage combine tests/*/.coverage
coverage report -m
2 changes: 2 additions & 0 deletions tests/custom_static_root/test_static_root.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ async def test_docs(client: AsyncClient): # nosec
text = response.text
assert response.status_code == 200, text
assert "/static/redoc" in text
response = await client.get("/app")
assert response.status_code == 200
8 changes: 5 additions & 3 deletions tests/defined_root_path/test_root_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ async def test_docs(client: AsyncClient): # nosec
text = response.text
assert response.status_code == 200, text
assert default_favicon_url in text
assert js_url in text
assert css_url in text
assert f'"{js_url}"' in text
assert f'"{css_url}"' in text
response = await client.get("/redoc")
text = response.text
assert response.status_code == 200, text
assert "/api/v1/static/redoc" in text
assert '"/api/v1/static/redoc' in text
response = await client.get("/app")
assert response.status_code == 200
2 changes: 2 additions & 0 deletions tests/favicon_online_cdn/test_race_favicon.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,5 @@ async def test_docs(client: AsyncClient): # nosec
text2 = response2.text
assert response2.status_code == 200, text2
assert urls.redoc in text2
response = await client.get("/app")
assert response.status_code == 200
2 changes: 2 additions & 0 deletions tests/online_cdn/test_race.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ async def test_docs(client: AsyncClient): # nosec
text2 = response2.text
assert response2.status_code == 200, text2
assert urls.redoc in text2
response = await client.get("/app")
assert response.status_code == 200
21 changes: 21 additions & 0 deletions tests/root_path_without_static/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env python

from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse

from fastapi_cdn_host import monkey_patch_for_docs_ui

app = FastAPI(title="FastAPI CDN host test", root_path="/api/v1")


@app.get("/", include_in_schema=False)
async def to_docs():
return RedirectResponse("/docs")


@app.get("/app")
async def get_app(request: Request) -> dict:
return {"routes": str(request.app.routes)}


monkey_patch_for_docs_ui(app)
36 changes: 36 additions & 0 deletions tests/root_path_without_static/test_root_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# mypy: no-disallow-untyped-decorators
import pytest
from httpx import AsyncClient
from main import app

from fastapi_cdn_host.client import CdnHostBuilder

default_favicon_url = "https://fastapi.tiangolo.com/img/favicon.png"


@pytest.fixture(scope="module")
def anyio_backend():
return "asyncio"


@pytest.fixture(scope="module")
async def client():
async with AsyncClient(app=app, base_url="http://test") as c:
yield c


@pytest.mark.anyio
async def test_docs(client: AsyncClient): # nosec
response = await client.get("/docs")
text = response.text
assert response.status_code == 200, text
assert default_favicon_url in text
urls = await CdnHostBuilder.sniff_the_fastest()
assert f'"{urls.js}"' in text
assert f'"{urls.css}"' in text
response2 = await client.get("/redoc")
text2 = response2.text
assert response2.status_code == 200, text2
assert f'"{urls.redoc}"' in text2
response = await client.get("/app")
assert response.status_code == 200
2 changes: 2 additions & 0 deletions tests/static_auto/test_auto_mount.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ async def test_docs(client: AsyncClient): # nosec
text = response.text
assert response.status_code == 200, text
assert "/static/redoc" in text
response = await client.get("/app")
assert response.status_code == 200
21 changes: 21 additions & 0 deletions tests/static_favicon_without_swagger_ui/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env python

from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse

from fastapi_cdn_host import monkey_patch_for_docs_ui

app = FastAPI(title="FastAPI CDN host test")


@app.get("/", include_in_schema=False)
async def to_docs():
return RedirectResponse("/docs")


@app.get("/app")
async def get_app(request: Request) -> dict:
return {"routes": str(request.app.routes)}


monkey_patch_for_docs_ui(app, favicon_url="/static/favicon.ico")
Binary file not shown.
39 changes: 39 additions & 0 deletions tests/static_favicon_without_swagger_ui/test_favicon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# mypy: no-disallow-untyped-decorators
import pytest
from httpx import AsyncClient
from main import app

from fastapi_cdn_host.client import CdnHostBuilder

default_favicon_url = "https://fastapi.tiangolo.com/img/favicon.png"


@pytest.fixture(scope="module")
def anyio_backend():
return "asyncio"


@pytest.fixture(scope="module")
async def client():
async with AsyncClient(app=app, base_url="http://test") as c:
yield c


@pytest.mark.anyio
async def test_docs(client: AsyncClient): # nosec
response = await client.get("/docs")
text = response.text
assert response.status_code == 200, text
assert default_favicon_url not in text
assert '"/static/favicon.ico"' in text
urls = await CdnHostBuilder.sniff_the_fastest()
assert f'"{urls.js}"' in text
assert f'"{urls.css}"' in text
response2 = await client.get("/redoc")
text2 = response2.text
assert response2.status_code == 200, text2
assert f'"{urls.redoc}"' in text2
response = await client.get("/static/favicon.ico")
assert response.status_code == 200
response = await client.get("/app")
assert response.status_code == 200
2 changes: 2 additions & 0 deletions tests/static_mounted/test_mounted.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ async def test_docs(client: AsyncClient): # nosec
text = response.text
assert response.status_code == 200, text
assert "/static/redoc" in text
response = await client.get("/app")
assert response.status_code == 200
2 changes: 2 additions & 0 deletions tests/static_with_favicon/test_auto_find_favicon.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ async def test_docs(client: AsyncClient): # nosec
text = response.text
assert response.status_code == 200, text
assert "/static/redoc" in text
response = await client.get("/app")
assert response.status_code == 200

0 comments on commit a9b5bbf

Please sign in to comment.