├── .coverage ├── .flake8 ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── .mypy.ini ├── CHANGELOG.md ├── LICENSE ├── README.md ├── fastapi_offline ├── __init__.py ├── consts.py ├── core.py └── py.typed ├── setup.py └── tests ├── __init__.py ├── test_altpaths.py ├── test_consts.py ├── test_core.py ├── test_favicon.py ├── test_subapps.py └── test_subapps2.py /.coverage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turettn/fastapi_offline/630b4de868f0f02eaf62f07db871eec6bd1650ec/.coverage -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | 8 | - package-ecosystem: 'github-actions' 9 | directory: '/' 10 | schedule: 11 | interval: daily -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | types: [opened, synchronize] 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 14 | fail-fast: false 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install Dependencies 23 | run: pip install flit setuptools 24 | - name: Install package, dependencies, and test tools 25 | run: | 26 | python setup.py sdist 27 | python -m pip install `ls -1 dist/*.tar.gz`[test] 28 | python -m pip install pytest pytest-cov flake8 black mypy 29 | - name: Check black compliance 30 | run : | 31 | black --check fastapi_offline 32 | - name: Run linters 33 | run : | 34 | flake8 fastapi_offline 35 | mypy fastapi_offline 36 | - name: Run unit tests 37 | run: | 38 | pytest tests --cov=fastapi_offline --cov-report=term-missing --cov-report=xml 39 | - name: Upload coverage 40 | uses: codecov/codecov-action@v5 41 | with: 42 | token: ${{ secrets.CODECOV_TOKEN }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*.egg-info 2 | /fastapi_offline/static/* 3 | build/ 4 | dist/ 5 | */__pycache__/ 6 | coverage.xml 7 | venv/ -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | strict = True 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All versions grab the newest version of the external dependencies (Swagger, ReDoc). 4 | 5 | ## 1.0.0 - 2021-03-18 6 | - Initial Commit 7 | 8 | ## 1.1.0 - 2021-04-27 9 | - Respects `root_path` to allow for sub-applications 10 | 11 | # 1.2.0 - 2021-05-16 12 | - Bundle default shortcut link 13 | - Allow configurable shortcut link 14 | - Redoc: Use local fonts instead of google fonts 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Neal Turett 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | [![](https://img.shields.io/pypi/v/fastapi-offline.svg)](https://pypi.python.org/pypi/fastapi-offline/) 4 | [![Test](https://github.com/turettn/fastapi_offline/actions/workflows/test.yml/badge.svg)](https://github.com/turettn/fastapi_offline/actions/workflows/test.yml) 5 | [![codecov](https://codecov.io/gh/turettn/fastapi_offline/branch/main/graph/badge.svg)](https://codecov.io/gh/turettn/fastapi_offline) 6 | 7 | [FastAPI](https://fastapi.tiangolo.com/) is awesome, but the documentation pages (Swagger or Redoc) all depend on external CDNs, which is problematic if you want to run on disconnected networks. 8 | 9 | This package includes the required files from the CDN and serves them locally. It also provides a super-simple way to get a FastAPI instance configured to use those files. 10 | 11 | Under the hood, this simply automates the process described in the official documentation [here](https://fastapi.tiangolo.com/advanced/extending-openapi/#self-hosting-javascript-and-css-for-docs). 12 | 13 | # Installation 14 | 15 | You can install this package from PyPi: 16 | 17 | ```bash 18 | pip install fastapi-offline 19 | ``` 20 | 21 | # Example 22 | 23 | Given the example from the [FastAPI tutorial](https://fastapi.tiangolo.com/tutorial/first-steps/): 24 | 25 | ```python 26 | from fastapi import FastAPI 27 | 28 | app = FastAPI() 29 | 30 | 31 | @app.get("/") 32 | async def root(): 33 | return {"message": "Hello World"} 34 | ``` 35 | 36 | Simply create a `fastapi_offline.FastAPIOffline` object instead: 37 | 38 | ```python 39 | from fastapi_offline import FastAPIOffline 40 | 41 | app = FastAPIOffline() 42 | 43 | 44 | @app.get("/") 45 | async def root(): 46 | return {"message": "Hello World"} 47 | ``` 48 | 49 | Any options passed to `FastAPIOffline()` except `docs_url`, `redoc_url`, `favicon_url`, and `static_url` are passed through to `FastAPI()`. `docs_url` and `redoc_url` are handled by `fastapi-offline`, and use the same syntax as normal `fastapi` library. 50 | 51 | `static_url` can be used to set the path for the static js/css files, e.g. `static_url=/static-files` (default: `/static-offline-docs`). 52 | 53 | # Using a custom shortcut icon 54 | 55 | By default, the FastAPI `favicon.png` is included and used as the shortcut icon on the docs pages. If you want to use a different one, you can specify it with the `favicon_url` argument: 56 | 57 | ```py 58 | app = FastAPIOffline( 59 | favicon_url="http://my.cool.site/favicon.png" 60 | ) 61 | ``` 62 | 63 | # Licensing 64 | 65 | * This code is released under the MIT license. 66 | * Parts of Swagger are included in this package. The original license ([Apache 2.0](https://swagger.io/license/)) and copyright apply to those files. 67 | * Parts of Redoc are included in this package. The original license ([MIT](https://github.com/Redocly/redoc/blob/master/LICENSE)) and copyright apply to those files. 68 | * The FastAPI `favicon.png` file is included in this package. The original license ([MIT](https://github.com/Redocly/redoc/blob/master/LICENSE)) and copyright apply to that file. -------------------------------------------------------------------------------- /fastapi_offline/__init__.py: -------------------------------------------------------------------------------- 1 | """Provide non-CDN-dependent Swagger & Redoc pages to FastAPI""" 2 | 3 | from .core import FastAPIOffline 4 | 5 | __all__ = [ 6 | "FastAPIOffline", 7 | ] 8 | -------------------------------------------------------------------------------- /fastapi_offline/consts.py: -------------------------------------------------------------------------------- 1 | SWAGGER_JS = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js" 2 | SWAGGER_CSS = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css" 3 | REDOC_JS = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js" 4 | FAVICON = "https://fastapi.tiangolo.com/img/favicon.png" 5 | -------------------------------------------------------------------------------- /fastapi_offline/core.py: -------------------------------------------------------------------------------- 1 | """Provide non-CDN-dependent Swagger & Redoc pages to FastAPI""" 2 | 3 | from pathlib import Path 4 | from typing import Any, Optional 5 | 6 | from fastapi import FastAPI, Request 7 | from fastapi.openapi.docs import ( 8 | get_redoc_html, 9 | get_swagger_ui_html, 10 | get_swagger_ui_oauth2_redirect_html, 11 | ) 12 | from fastapi.staticfiles import StaticFiles 13 | from starlette.responses import HTMLResponse 14 | 15 | _STATIC_PATH = Path(__file__).parent / "static" 16 | 17 | 18 | def FastAPIOffline( 19 | docs_url: Optional[str] = "/docs", 20 | redoc_url: Optional[str] = "/redoc", 21 | *args: Any, 22 | **kwargs: Any, 23 | ) -> FastAPI: 24 | """Return a FastAPI obj that doesn't rely on CDN for the documentation page""" 25 | # Disable the normal doc & redoc pages 26 | kwargs["docs_url"] = None 27 | kwargs["redoc_url"] = None 28 | 29 | # Grab the user specified favicon url (if present) 30 | favicon_url = kwargs.pop("favicon_url", None) 31 | 32 | # Set path to to static files or default to /static-offline-docs 33 | static_url = kwargs.pop("static_url", "/static-offline-docs") 34 | 35 | # Create the FastAPI object 36 | app = FastAPI(*args, **kwargs) 37 | 38 | # This mostly just keeps mypy happy 39 | assert isinstance(app.openapi_url, str) 40 | assert isinstance(app.swagger_ui_oauth2_redirect_url, str) 41 | 42 | openapi_url = app.openapi_url 43 | swagger_ui_oauth2_redirect_url = app.swagger_ui_oauth2_redirect_url 44 | 45 | # Set up static file mount 46 | app.mount( 47 | static_url, 48 | StaticFiles(directory=_STATIC_PATH.as_posix()), 49 | name="static-offline-docs", 50 | ) 51 | 52 | if docs_url is not None: 53 | # Define the doc and redoc pages, pointing at the right files 54 | @app.get(docs_url, include_in_schema=False) 55 | async def custom_swagger_ui_html(request: Request) -> HTMLResponse: 56 | root = request.scope.get("root_path") 57 | 58 | if favicon_url is None: 59 | favicon = f"{root}{static_url}/favicon.png" 60 | else: 61 | favicon = favicon_url 62 | 63 | return get_swagger_ui_html( 64 | openapi_url=f"{root}{openapi_url}", 65 | title=app.title + " - Swagger UI", 66 | oauth2_redirect_url=swagger_ui_oauth2_redirect_url, 67 | swagger_js_url=f"{root}{static_url}/swagger-ui-bundle.js", 68 | swagger_css_url=f"{root}{static_url}/swagger-ui.css", 69 | swagger_favicon_url=favicon, 70 | swagger_ui_parameters=app.swagger_ui_parameters, 71 | ) 72 | 73 | @app.get(swagger_ui_oauth2_redirect_url, include_in_schema=False) 74 | async def swagger_ui_redirect() -> HTMLResponse: 75 | return get_swagger_ui_oauth2_redirect_html() 76 | 77 | if redoc_url is not None: 78 | 79 | @app.get(redoc_url, include_in_schema=False) 80 | async def redoc_html(request: Request) -> HTMLResponse: 81 | root = request.scope.get("root_path") 82 | 83 | if favicon_url is None: 84 | favicon = f"{root}{static_url}/favicon.png" 85 | else: 86 | favicon = favicon_url 87 | 88 | return get_redoc_html( 89 | openapi_url=f"{root}{openapi_url}", 90 | title=app.title + " - ReDoc", 91 | redoc_js_url=f"{root}{static_url}/redoc.standalone.js", 92 | with_google_fonts=False, 93 | redoc_favicon_url=favicon, 94 | ) 95 | 96 | # Return the FastAPI object 97 | return app 98 | -------------------------------------------------------------------------------- /fastapi_offline/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turettn/fastapi_offline/630b4de868f0f02eaf62f07db871eec6bd1650ec/fastapi_offline/py.typed -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from urllib.request import build_opener, install_opener, urlretrieve 3 | 4 | from setuptools import setup 5 | from setuptools.command.sdist import sdist 6 | 7 | __version__ = "1.7.3" 8 | 9 | 10 | BASE_PATH = Path(__file__).parent 11 | README = (BASE_PATH / "README.md").read_text() 12 | FASTAPI_VER = "fastapi>=0.99.0" 13 | TEST_DEPS = ["pytest", "requests", "starlette[full]"] 14 | 15 | 16 | class SDistWrapper(sdist): 17 | def run(self) -> None: 18 | "Download files into static/, then pass through to normal install" 19 | from fastapi_offline.consts import FAVICON, REDOC_JS, SWAGGER_CSS, SWAGGER_JS 20 | 21 | # Find ourself 22 | 23 | static_path = BASE_PATH / "fastapi_offline" / "static" 24 | static_path.mkdir(exist_ok=True) 25 | 26 | # Set a header to avoid cloudflare's bot blocker 27 | opener = build_opener() 28 | opener.addheaders = [ 29 | ("User-agent", f"fastapi-offline-packager / {__version__}") 30 | ] 31 | install_opener(opener) 32 | 33 | # Download files 34 | for download in ( 35 | SWAGGER_JS, 36 | SWAGGER_CSS, 37 | REDOC_JS, 38 | FAVICON, 39 | ): 40 | urlretrieve(download, static_path / download.split("/")[-1]) 41 | 42 | sdist.run(self) 43 | 44 | 45 | setup( 46 | name="fastapi_offline", 47 | version=__version__, 48 | author="Neal Turett", 49 | author_email="turettn@gmail.com", 50 | description="FastAPI without reliance on CDNs for docs", 51 | long_description=README, 52 | long_description_content_type="text/markdown", 53 | url="https://github.com/turettn/fastapi_offline", 54 | license="MIT", 55 | classifiers=[ 56 | "License :: OSI Approved :: MIT License", 57 | "Programming Language :: Python :: 3", 58 | "Programming Language :: Python :: 3 :: Only", 59 | ], 60 | packages=["fastapi_offline"], 61 | package_data={"fastapi_offline": ["static/*", "py.typed"]}, 62 | python_requires=">=3.8", 63 | install_requires=[FASTAPI_VER], 64 | tests_require=TEST_DEPS, 65 | setup_requires=[FASTAPI_VER], 66 | extras_require={"test": TEST_DEPS}, 67 | cmdclass={"sdist": SDistWrapper}, 68 | ) 69 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/turettn/fastapi_offline/630b4de868f0f02eaf62f07db871eec6bd1650ec/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_altpaths.py: -------------------------------------------------------------------------------- 1 | from fastapi.testclient import TestClient 2 | from fastapi import FastAPI 3 | from fastapi_offline import FastAPIOffline 4 | 5 | DOC = "/asdf" 6 | REDOC = "/qwerty" 7 | STATIC = "/azerty" 8 | 9 | # Custom paths for all 10 | app1 = FastAPIOffline(docs_url=DOC, redoc_url=REDOC, static_url=STATIC) 11 | client1 = TestClient(app1) 12 | 13 | # Disable redoc 14 | app2 = FastAPIOffline(docs_url=DOC, redoc_url=None) 15 | client2 = TestClient(app2) 16 | 17 | # Disable Swagger 18 | app3 = FastAPIOffline(docs_url=None, redoc_url=REDOC) 19 | client3 = TestClient(app3) 20 | 21 | 22 | def test_swagger(): 23 | """Make sure Swagger appears at the right place""" 24 | assert client1.get("/docs").status_code == 404 25 | assert client1.get(DOC).status_code == 200 26 | 27 | assert client2.get("/docs").status_code == 404 28 | assert client2.get(DOC).status_code == 200 29 | 30 | assert client3.get("/docs").status_code == 404 31 | assert client3.get(DOC).status_code == 404 32 | 33 | 34 | def test_custom_redocs(): 35 | """Make sure Redoc appears at the right place""" 36 | assert client1.get("/redoc").status_code == 404 37 | assert client1.get(REDOC).status_code == 200 38 | 39 | assert client2.get("/redoc").status_code == 404 40 | assert client2.get(REDOC).status_code == 404 41 | 42 | assert client3.get("/redoc").status_code == 404 43 | assert client3.get(REDOC).status_code == 200 44 | 45 | def test_static(): 46 | """Make sure static files appears at the right place""" 47 | for static_file in ["swagger-ui-bundle.js", "swagger-ui.css", "redoc.standalone.js"]: 48 | assert client1.get(f"/static-offline-docs/{static_file}").status_code == 404 49 | assert client1.get(f"{STATIC}/{static_file}").status_code == 200 50 | 51 | assert client2.get(f"/static-offline-docs/{static_file}").status_code == 200 52 | assert client2.get(f"{STATIC}/{static_file}").status_code == 404 53 | -------------------------------------------------------------------------------- /tests/test_consts.py: -------------------------------------------------------------------------------- 1 | "Check the default FastAPI package to confirm our consts" 2 | import inspect 3 | 4 | from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html 5 | 6 | from fastapi_offline.consts import * 7 | 8 | 9 | def test_swagger(): 10 | params = inspect.signature(get_swagger_ui_html).parameters 11 | assert params["swagger_js_url"].default == SWAGGER_JS 12 | assert params["swagger_css_url"].default == SWAGGER_CSS 13 | assert params["swagger_favicon_url"].default == FAVICON 14 | 15 | 16 | def test_redoc(): 17 | params = inspect.signature(get_redoc_html).parameters 18 | assert params["redoc_js_url"].default == REDOC_JS 19 | assert params["redoc_favicon_url"].default == FAVICON 20 | -------------------------------------------------------------------------------- /tests/test_core.py: -------------------------------------------------------------------------------- 1 | from fastapi.testclient import TestClient 2 | from fastapi_offline import FastAPIOffline 3 | 4 | # Create an application 5 | app = FastAPIOffline() 6 | 7 | 8 | @app.get("/") 9 | async def root(): 10 | return {"message": "Hello World"} 11 | 12 | 13 | # Create a test client 14 | client = TestClient(app) 15 | 16 | 17 | def test_read_main(): 18 | response = client.get("/") 19 | assert response.status_code == 200 20 | assert response.json() == {"message": "Hello World"} 21 | 22 | 23 | # Check the docs pages 24 | def test_read_docs(): 25 | for page in ["/docs", "/redoc"]: 26 | response = client.get(page) 27 | assert response.status_code == 200 28 | assert "cdn.jsdelivr.net" not in response.text 29 | assert "static-offline-docs" in response.text 30 | 31 | 32 | # Check the static pages 33 | def test_read_statics(): 34 | for page in [ 35 | "swagger-ui-bundle.js", 36 | "swagger-ui.css", 37 | "redoc.standalone.js", 38 | "favicon.png", 39 | ]: 40 | response = client.get("/static-offline-docs/" + page) 41 | assert response.status_code == 200 42 | -------------------------------------------------------------------------------- /tests/test_favicon.py: -------------------------------------------------------------------------------- 1 | from fastapi.testclient import TestClient 2 | from fastapi_offline import FastAPIOffline 3 | 4 | # Create an application 5 | app1 = FastAPIOffline() 6 | client1 = TestClient(app1) 7 | 8 | FAVICON = "http://fake.site/favicon.png" 9 | app2 = FastAPIOffline(favicon_url=FAVICON) 10 | client2 = TestClient(app2) 11 | 12 | app2.mount("/app1", app1) 13 | 14 | app3 = FastAPIOffline(favicon_url=None) 15 | client3 = TestClient(app3) 16 | 17 | 18 | def test_normal_redoc(): 19 | resp = client1.get("/redoc") 20 | assert "/static-offline-docs/favicon.png" in resp.text 21 | 22 | 23 | def test_custom_redoc(): 24 | resp = client2.get("/redoc") 25 | assert FAVICON in resp.text 26 | 27 | 28 | def test_normal_swagger(): 29 | resp = client1.get("/docs") 30 | assert "/static-offline-docs/favicon.png" in resp.text 31 | 32 | 33 | def test_custom_swagger(): 34 | resp = client2.get("/docs") 35 | assert FAVICON in resp.text 36 | 37 | 38 | def test_normal_submounted_redoc(): 39 | resp = client2.get("/app1/redoc") 40 | print(resp.text) 41 | assert "/app1/static-offline-docs/favicon.png" in resp.text 42 | 43 | 44 | def test_normal_submounted_swagger(): 45 | resp = client2.get("/app1/docs") 46 | assert "/app1/static-offline-docs/favicon.png" in resp.text 47 | -------------------------------------------------------------------------------- /tests/test_subapps.py: -------------------------------------------------------------------------------- 1 | from fastapi.testclient import TestClient 2 | from fastapi import FastAPI 3 | from fastapi_offline import FastAPIOffline 4 | 5 | # Create an application with docs 6 | app = FastAPIOffline() 7 | 8 | 9 | @app.get("/") 10 | async def root(): 11 | return {"message": "Hello World"} 12 | 13 | 14 | # Create a sub-application with no docs 15 | subapp_no_docs = FastAPI(openapi_url=None) 16 | 17 | 18 | @subapp_no_docs.get("/") 19 | async def sub_root(): 20 | return {"message": "Goodbye World"} 21 | 22 | 23 | app.mount("/sub_no_docs", subapp_no_docs) 24 | 25 | # Create a sub-application with docs 26 | subapp_yes_docs = FastAPIOffline() 27 | 28 | 29 | @subapp_yes_docs.get("/") 30 | async def sub2_root(): 31 | return {"message": "Congrats World"} 32 | 33 | 34 | app.mount("/sub_yes_docs", subapp_yes_docs) 35 | 36 | # Create a test client 37 | client = TestClient(app) 38 | 39 | # Check the main app still works 40 | def test_read_main(): 41 | """Hello World""" 42 | response = client.get("/") 43 | assert response.status_code == 200 44 | assert response.json() == {"message": "Hello World"} 45 | 46 | 47 | # Test the sub apps work 48 | def test_sub_app(): 49 | """Goodbye World""" 50 | response = client.get("/sub_no_docs/") 51 | assert response.status_code == 200 52 | assert response.json() == {"message": "Goodbye World"} 53 | 54 | 55 | def test_sub2_app(): 56 | """Congrats World""" 57 | response = client.get("/sub_yes_docs/") 58 | assert response.status_code == 200 59 | assert response.json() == {"message": "Congrats World"} 60 | 61 | 62 | # Check the docs pages on the main app 63 | def test_read_docs(): 64 | for page in ["/docs", "/redoc"]: 65 | response = client.get(page) 66 | assert response.status_code == 200 67 | assert "cdn.jsdelivr.net" not in response.text 68 | assert "static-offline-docs" in response.text 69 | 70 | 71 | # Check the static pages on the main app 72 | def test_read_statics(): 73 | for page in ["swagger-ui-bundle.js", "swagger-ui.css", "redoc.standalone.js"]: 74 | response = client.get("/static-offline-docs/" + page) 75 | assert response.status_code == 200 76 | 77 | 78 | # Make sure that the first subapp doesn't have docs 79 | def test_subapp_no_docs(): 80 | response = client.get("/sub_no_docs/docs") 81 | assert response.status_code == 404 82 | 83 | 84 | # Make sure the second does 85 | def test_subapp_yes_docs(): 86 | for page in ["sub_yes_docs/docs", "sub_yes_docs/redoc"]: 87 | response = client.get(page) 88 | assert response.status_code == 200 89 | assert "cdn.jsdelivr.net" not in response.text 90 | assert "sub_yes_docs/static-offline-docs" in response.text 91 | -------------------------------------------------------------------------------- /tests/test_subapps2.py: -------------------------------------------------------------------------------- 1 | from fastapi.testclient import TestClient 2 | from fastapi import FastAPI 3 | from fastapi_offline import FastAPIOffline 4 | 5 | # Create an application with no docs 6 | app = FastAPI(openapi_url=None) 7 | 8 | 9 | @app.get("/") 10 | async def root(): 11 | return {"message": "Hello World"} 12 | 13 | 14 | # Create a sub-app with docs 15 | 16 | sub_app = FastAPIOffline() 17 | 18 | 19 | @sub_app.get("/") 20 | async def subroot(): 21 | return {"message": "Goodbye World"} 22 | 23 | 24 | app.mount("/sub", sub_app) 25 | 26 | 27 | # Create a test client 28 | client = TestClient(app) 29 | 30 | 31 | # Test the actual endpoints 32 | def test_read_main(): 33 | response = client.get("/") 34 | assert response.status_code == 200 35 | assert response.json() == {"message": "Hello World"} 36 | 37 | 38 | def test_sub_main(): 39 | response = client.get("/sub/") 40 | assert response.status_code == 200 41 | assert response.json() == {"message": "Goodbye World"} 42 | 43 | 44 | # Check the docs pages on main are disabled 45 | def test_disabled_docs(): 46 | for page in ["/docs", "/redoc"]: 47 | response = client.get(page) 48 | assert response.status_code == 404 49 | 50 | 51 | # Check the docs pages on the subapp 52 | def test_read_docs(): 53 | for page in ["/sub/docs", "/sub/redoc"]: 54 | response = client.get(page) 55 | assert response.status_code == 200 56 | assert "cdn.jsdelivr.net" not in response.text 57 | assert "sub/static-offline-docs" in response.text 58 | 59 | 60 | # Check the static pages 61 | def test_read_statics(): 62 | for page in ["swagger-ui-bundle.js", "swagger-ui.css", "redoc.standalone.js"]: 63 | response = client.get("/sub/static-offline-docs/" + page) 64 | assert response.status_code == 200 65 | --------------------------------------------------------------------------------