From eca5cfb9417c7b998de9f0f42269a555e14dcf69 Mon Sep 17 00:00:00 2001 From: Waket Zheng Date: Fri, 29 Dec 2023 12:08:42 +0800 Subject: [PATCH] Support extend default cdn hosts --- README.md | 2 +- README.zh-hans.md | 11 ++--- fastapi_cdn_host/client.py | 19 ++++++-- pyproject.toml | 2 +- scripts/test.sh | 1 + tests/extend_choices/main.py | 33 ++++++++++++++ tests/extend_choices/test_extend.py | 45 +++++++++++++++++++ tests/favicon_online_cdn/test_race_favicon.py | 2 +- tests/online_cdn/test_online_race.py | 2 +- .../test_root_path.py | 2 +- .../test_favicon.py | 2 +- 11 files changed, 106 insertions(+), 15 deletions(-) create mode 100644 tests/extend_choices/main.py create mode 100644 tests/extend_choices/test_extend.py diff --git a/README.md b/README.md index c169302..5408069 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ async def redoc_html(): # http://my-cdn.com/swagger-ui@latest/swagger-ui-bundle.js # http://my-cdn.com/swagger-ui@latest/swagger-ui.css # render /redoc with: `http://my-cdn.com/redoc/next/redoc.standalone.js` -monkey_patch_for_docs_ui(app, cdn_host=('http://my-cdn.com', ('/swagger-ui@latest/', '/redoc/next/'))) +monkey_patch_for_docs_ui(app, docs_cdn_host=('http://my-cdn.com', ('/swagger-ui@latest/', '/redoc/next/'))) ``` ## License diff --git a/README.zh-hans.md b/README.zh-hans.md index 8efd7c4..3ac60ea 100644 --- a/README.zh-hans.md +++ b/README.zh-hans.md @@ -35,16 +35,17 @@ monkey_patch_for_docs_ui(app) 没有的话,使用协程并发对比https://cdn.jsdelivr.net、https://unpkg.com、https://cdnjs.cloudflare.com 三个CDN的响应速度,然后自动采用速度最快的那个。 -## 其他CDN +## 加入其他CDN作为备选 - 参考:https://github.com/lecepin/blog/blob/main/%E5%9B%BD%E5%86%85%E9%AB%98%E9%80%9F%E5%89%8D%E7%AB%AF%20Unpkg%20CDN%20%E6%9B%BF%E4%BB%A3%E6%96%B9%E6%A1%88.md ```py +from fastapi_cdn_host import CdnHostEnum monkey_patch_for_docs_ui( app, - cdn_host=( - 'https://cdn.bootcdn.net/ajax/libs', ('/swagger-ui/{version}/', '') # BootCDN - # 'https://cdn.staticfile.org', ('/swagger-ui/{version}/', '') # 七牛云 - # 'https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M', ('/swagger-ui/{version}/', '') # 字节 + docs_cdn_host=CdnHostEnum.extend( + ('https://cdn.bootcdn.net/ajax/libs', ('/swagger-ui/{version}/', '')), # BootCDN + ('https://cdn.staticfile.org', ('/swagger-ui/{version}/', '')), # 七牛云 + ('https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M', ('/swagger-ui/{version}/', '')), # 字节 ) ) ``` diff --git a/fastapi_cdn_host/client.py b/fastapi_cdn_host/client.py index 69f59fa..c6d84b1 100644 --- a/fastapi_cdn_host/client.py +++ b/fastapi_cdn_host/client.py @@ -22,10 +22,11 @@ Annotated[str, "redoc path or url info(must startswith '/')"], ] CdnDomainType = Annotated[str, "Host for swagger-ui/redoc"] +StrictCdnHostInfoType = Tuple[CdnDomainType, CdnPathInfoType] CdnHostInfoType = Union[ Annotated[CdnDomainType, f"Will use DEFAULT_ASSET_PATH: {DEFAULT_ASSET_PATH}"], - Tuple[CdnDomainType, CdnPathInfoType], Tuple[CdnDomainType, Annotated[str, "In case of swagger/redoc has the same path"]], + StrictCdnHostInfoType, ] @@ -37,6 +38,10 @@ class CdnHostEnum(Enum): OFFICIAL_REDOC, ) + @classmethod + def extend(cls, *host: StrictCdnHostInfoType) -> List[CdnHostInfoType]: + return [*host, *cls] + @dataclass class AssetUrl: @@ -118,6 +123,8 @@ def run(self) -> AssetUrl: cdn_host = cdn_host.value if isinstance(cdn_host, str): return self.build_asset_url(cdn_host, favicon_url=favicon) + if isinstance(cdn_host, list) and isinstance(cdn_host[0], tuple): + return self.run_async(self.sniff_the_fastest, favicon, cdn_host) cdn_host, asset_path = cdn_host if isinstance(asset_path, str): asset_path = (asset_path, asset_path) @@ -160,8 +167,10 @@ def build_race_data( return css_urls, they @classmethod - async def sniff_the_fastest(cls, favicon_url=None) -> AssetUrl: - css_urls, they = cls.build_race_data(list(CdnHostEnum)) + async def sniff_the_fastest( + cls, favicon_url=None, choices=list(CdnHostEnum) + ) -> AssetUrl: + css_urls, they = cls.build_race_data(choices) fast_css_url = await HttpSpider.find_fastest_host(css_urls) fast_host, fast_asset_path = they[css_urls.index(fast_css_url)] logger.info(f"Select cdn: {fast_host[0]} to serve swagger css/js") @@ -345,7 +354,9 @@ def detect_local_file( def monkey_patch_for_docs_ui( app: FastAPI, - docs_cdn_host: Union[CdnHostEnum, CdnHostInfoType, Path, None] = None, + docs_cdn_host: Union[ + CdnHostEnum, List[CdnHostInfoType], CdnHostInfoType, Path, None + ] = None, favicon_url: Union[str, None] = None, ) -> None: """Use local static files or the faster CDN host for docs asset(swagger-ui) diff --git a/pyproject.toml b/pyproject.toml index 554c041..e9266a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "fastapi-cdn-host" -version = "0.3.1" +version = "0.3.2" description = "" authors = ["Waket Zheng "] readme = "README.md" diff --git a/scripts/test.sh b/scripts/test.sh index 1817ff7..09f3eb6 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -18,6 +18,7 @@ cd ../private_cdn && coverage run -m pytest test_*.py cd ../cdn_with_default_asset_path && coverage run -m pytest test_*.py cd ../explicit_cdn_host && coverage run -m pytest test_*.py cd ../simple_asset_path && coverage run -m pytest test_*.py +cd ../extend_choices && coverage run -m pytest test_*.py cd ../.. && coverage combine tests/*/.coverage coverage report -m diff --git a/tests/extend_choices/main.py b/tests/extend_choices/main.py new file mode 100644 index 0000000..8a76ddc --- /dev/null +++ b/tests/extend_choices/main.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +from fastapi import FastAPI, Request +from fastapi.responses import RedirectResponse + +from fastapi_cdn_host import CdnHostEnum, 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, + docs_cdn_host=CdnHostEnum.extend( + ( + "https://cdn.bootcdn.net/ajax/libs", + ("/swagger-ui/{version}/", ""), + ), # BootCDN + ("https://cdn.staticfile.org", ("/swagger-ui/{version}/", "")), # 七牛云 + ( + "https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M", + ("/swagger-ui/{version}/", ""), + ), # 字节 + ), +) diff --git a/tests/extend_choices/test_extend.py b/tests/extend_choices/test_extend.py new file mode 100644 index 0000000..adda24f --- /dev/null +++ b/tests/extend_choices/test_extend.py @@ -0,0 +1,45 @@ +# mypy: no-disallow-untyped-decorators +import pytest +from httpx import AsyncClient +from main import app + +from fastapi_cdn_host.client import CdnHostBuilder, CdnHostEnum + + +@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 + urls = await CdnHostBuilder.sniff_the_fastest( + choices=CdnHostEnum.extend( + ( + "https://cdn.bootcdn.net/ajax/libs", + ("/swagger-ui/{version}/", ""), + ), + ("https://cdn.staticfile.org", ("/swagger-ui/{version}/", "")), + ( + "https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M", + ("/swagger-ui/{version}/", ""), + ), + ) + ) + response = await client.get("/docs") + text = response.text + assert response.status_code == 200, text + assert urls.js in text + assert urls.css in text + response2 = await client.get("/redoc") + text2 = response2.text + assert response2.status_code == 200, text2 + assert urls.redoc in text2 + response = await client.get("/app") + assert response.status_code == 200 diff --git a/tests/favicon_online_cdn/test_race_favicon.py b/tests/favicon_online_cdn/test_race_favicon.py index cd14792..01bd1e7 100644 --- a/tests/favicon_online_cdn/test_race_favicon.py +++ b/tests/favicon_online_cdn/test_race_favicon.py @@ -19,11 +19,11 @@ async def client(): @pytest.mark.anyio async def test_docs(client: AsyncClient): # nosec + urls = await CdnHostBuilder.sniff_the_fastest() response = await client.get("/docs") text = response.text assert response.status_code == 200, text assert '"https://ubuntu.com/favicon.ico"' in text - urls = await CdnHostBuilder.sniff_the_fastest() assert urls.js in text assert urls.css in text response2 = await client.get("/redoc") diff --git a/tests/online_cdn/test_online_race.py b/tests/online_cdn/test_online_race.py index 7f94cc5..098cb1f 100644 --- a/tests/online_cdn/test_online_race.py +++ b/tests/online_cdn/test_online_race.py @@ -21,11 +21,11 @@ async def client(): @pytest.mark.anyio async def test_docs(client: AsyncClient): # nosec + urls = await CdnHostBuilder.sniff_the_fastest() 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 urls.js in text assert urls.css in text response2 = await client.get("/redoc") diff --git a/tests/root_path_without_static/test_root_path.py b/tests/root_path_without_static/test_root_path.py index 4f3e803..46496ab 100644 --- a/tests/root_path_without_static/test_root_path.py +++ b/tests/root_path_without_static/test_root_path.py @@ -21,11 +21,11 @@ async def client(): @pytest.mark.anyio async def test_docs(client: AsyncClient): # nosec + urls = await CdnHostBuilder.sniff_the_fastest() 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") diff --git a/tests/static_favicon_without_swagger_ui/test_favicon.py b/tests/static_favicon_without_swagger_ui/test_favicon.py index 22d3599..2e5ea3b 100644 --- a/tests/static_favicon_without_swagger_ui/test_favicon.py +++ b/tests/static_favicon_without_swagger_ui/test_favicon.py @@ -21,12 +21,12 @@ async def client(): @pytest.mark.anyio async def test_docs(client: AsyncClient): # nosec + urls = await CdnHostBuilder.sniff_the_fastest() 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")