specbook-fastapi 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,99 @@
1
+ """Serve the Specbook API viewer from a FastAPI app.
2
+
3
+ from fastapi import FastAPI
4
+ from specbook_fastapi import add_specbook
5
+
6
+ app = FastAPI(docs_url=None, redoc_url=None)
7
+ add_specbook(app, route="/docs")
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from importlib.metadata import PackageNotFoundError
13
+ from importlib.metadata import version as _version
14
+ from typing import Any
15
+
16
+ from fastapi import FastAPI, Request
17
+ from fastapi.responses import HTMLResponse
18
+
19
+ from ._html import TESTED_VERSION, Cdn, render_embed, render_page, script_url
20
+
21
+ try:
22
+ __version__ = _version("specbook-fastapi")
23
+ except PackageNotFoundError: # running from a source checkout that is not installed
24
+ __version__ = "0.0.0"
25
+
26
+ __all__ = ["TESTED_VERSION", "add_specbook", "get_specbook_html", "__version__"]
27
+
28
+
29
+ def get_specbook_html(
30
+ *,
31
+ openapi_url: str = "/openapi.json",
32
+ title: str = "API docs",
33
+ default_env: str | None = None,
34
+ base_url: str | None = None,
35
+ cdn: Cdn = "specbook",
36
+ version: str | None = None,
37
+ host: str | None = None,
38
+ src: str | None = None,
39
+ nonce: str | None = None,
40
+ ) -> HTMLResponse:
41
+ """A page that loads the Specbook viewer for the OpenAPI document at `openapi_url`.
42
+
43
+ Args:
44
+ openapi_url: The OpenAPI document. A relative address is resolved against the page.
45
+ title: The page title. It is HTML-escaped.
46
+ default_env: The environment shown first: `mock` (the viewer's default), `live`, or the name of a server in the
47
+ document.
48
+ base_url: Where live requests go when the document lists no server. Default: the page's own origin.
49
+ cdn: Where the viewer is loaded from: `specbook` (the stable `v1` channel at ui.specbook.dev) or `npm`
50
+ (`@specbook/ui` on jsDelivr).
51
+ version: With `specbook`, the channel (default `v1`). With `npm`, a version or range of `@specbook/ui`
52
+ (default: the release this package was tested with).
53
+ host: The host to load from instead of the one `cdn` names, for example `https://unpkg.com` or an internal
54
+ mirror.
55
+ src: The full address of `specbook.js`, for a copy you serve yourself. It overrides `cdn`, `version` and `host`.
56
+ nonce: A CSP nonce for the viewer's script.
57
+ """
58
+ embed = render_embed(
59
+ spec=openapi_url,
60
+ script=script_url(cdn=cdn, host=host, version=version, src=src),
61
+ base_url=base_url,
62
+ default_env=default_env,
63
+ nonce=nonce,
64
+ )
65
+ return HTMLResponse(render_page(title=title, embed=embed))
66
+
67
+
68
+ def add_specbook(
69
+ app: FastAPI,
70
+ *,
71
+ route: str = "/specbook",
72
+ include_in_schema: bool = False,
73
+ **options: Any,
74
+ ) -> FastAPI:
75
+ """Serve the viewer at `route` for the app's own OpenAPI document.
76
+
77
+ The document's address and the page title come from the app (`app.openapi_url`, with the request's `root_path` in
78
+ front of it, so it works behind a prefix or a proxy, and `app.title`) unless you pass `openapi_url` or `title`. Any
79
+ other option is passed on to `get_specbook_html`. To replace FastAPI's own docs page, turn it off and take its
80
+ route::
81
+
82
+ app = FastAPI(docs_url=None, redoc_url=None)
83
+ add_specbook(app, route="/docs")
84
+ """
85
+ if "openapi_url" not in options and app.openapi_url is None:
86
+ raise ValueError(
87
+ "The app has no OpenAPI document (openapi_url is None). Pass openapi_url=... or enable the document."
88
+ )
89
+
90
+ @app.get(route, include_in_schema=include_in_schema, response_class=HTMLResponse)
91
+ async def specbook_docs(request: Request) -> HTMLResponse:
92
+ page = dict(options)
93
+ if "openapi_url" not in page:
94
+ root_path = request.scope.get("root_path", "").rstrip("/")
95
+ page["openapi_url"] = f"{root_path}{app.openapi_url}"
96
+ page.setdefault("title", app.title)
97
+ return get_specbook_html(**page)
98
+
99
+ return app
@@ -0,0 +1,92 @@
1
+ """The page that loads the Specbook viewer: pure functions, no FastAPI needed."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import html
6
+ import json
7
+ from typing import Any, Literal
8
+
9
+ Cdn = Literal["specbook", "npm"]
10
+
11
+ #: The `@specbook/ui` release the `npm` host loads unless `version` is given.
12
+ TESTED_VERSION = "0.6.0"
13
+
14
+ SPECBOOK_HOST = "https://ui.specbook.dev"
15
+ NPM_HOST = "https://cdn.jsdelivr.net/npm"
16
+ DEFAULT_CHANNEL = "v1"
17
+
18
+
19
+ def escape_html(value: str) -> str:
20
+ """Escape text for an HTML body or a quoted attribute."""
21
+ return html.escape(value, quote=True)
22
+
23
+
24
+ def json_for_script(value: Any) -> str:
25
+ """JSON that is safe inside a `<script>` element: nothing in it can close the element or start a comment."""
26
+ return (
27
+ json.dumps(value, separators=(",", ":"), ensure_ascii=False)
28
+ .replace("<", "\\u003c")
29
+ .replace(">", "\\u003e")
30
+ .replace("&", "\\u0026")
31
+ .replace("
", "\\u2028")
32
+ .replace("
", "\\u2029")
33
+ )
34
+
35
+
36
+ def script_url(
37
+ *,
38
+ cdn: Cdn = "specbook",
39
+ host: str | None = None,
40
+ version: str | None = None,
41
+ src: str | None = None,
42
+ ) -> str:
43
+ """The address of `specbook.js`. `src` wins; otherwise `cdn` picks the host and `version` the release."""
44
+ if src is not None:
45
+ return src
46
+ if cdn == "npm":
47
+ base = (host or NPM_HOST).rstrip("/")
48
+ return f"{base}/@specbook/ui@{version or TESTED_VERSION}/dist-viewer/specbook.js"
49
+ if cdn == "specbook":
50
+ base = (host or SPECBOOK_HOST).rstrip("/")
51
+ return f"{base}/{version or DEFAULT_CHANNEL}/specbook.js"
52
+ raise ValueError(f"cdn must be 'specbook' or 'npm', not {cdn!r}")
53
+
54
+
55
+ def render_embed(
56
+ *,
57
+ spec: str,
58
+ script: str,
59
+ base_url: str | None = None,
60
+ default_env: str | None = None,
61
+ nonce: str | None = None,
62
+ ) -> str:
63
+ """The three elements the viewer needs: its container, its configuration as data, and its module script."""
64
+ config: dict[str, str] = {"spec": spec}
65
+ if base_url is not None:
66
+ config["baseUrl"] = base_url
67
+ if default_env is not None:
68
+ config["defaultEnv"] = default_env
69
+ nonce_attr = "" if nonce is None else f' nonce="{escape_html(nonce)}"'
70
+ return "\n".join(
71
+ [
72
+ '<div id="app"></div>',
73
+ f'<script type="application/json" id="specbook-config">{json_for_script(config)}</script>',
74
+ f'<script type="module" src="{escape_html(script)}"{nonce_attr}></script>',
75
+ ]
76
+ )
77
+
78
+
79
+ def render_page(*, title: str, embed: str) -> str:
80
+ """A whole page around the embed. The viewer takes over the page, so it gets one of its own."""
81
+ return f"""<!doctype html>
82
+ <html lang="en">
83
+ <head>
84
+ <meta charset="utf-8" />
85
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
86
+ <title>{escape_html(title)}</title>
87
+ </head>
88
+ <body>
89
+ {embed}
90
+ </body>
91
+ </html>
92
+ """
File without changes
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.5
2
+ Name: specbook-fastapi
3
+ Version: 0.1.0
4
+ Summary: Serve the Specbook API viewer from a FastAPI app
5
+ Project-URL: Homepage, https://docs.specbook.dev
6
+ Project-URL: Viewer, https://ui.specbook.dev
7
+ Project-URL: Source, https://github.com/specbook-dev/specbook/tree/main/integrations/fastapi
8
+ Project-URL: Issues, https://github.com/specbook-dev/specbook/issues
9
+ Author: Ajai Shankar
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: api-docs,api-documentation,fastapi,openapi,specbook,swagger
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Framework :: FastAPI
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Topic :: Software Development :: Documentation
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: fastapi>=0.100
23
+ Description-Content-Type: text/markdown
24
+
25
+ # specbook-fastapi
26
+
27
+ Serves the [Specbook UI](https://ui.specbook.dev) for your FastAPI app's OpenAPI document. It reads the document in the browser and lets a visitor send each request: against **mock** responses built from the document's examples, or **live** against your API. It runs on your app's own origin, so there is no CORS to configure and the visitor's cookies are sent.
28
+
29
+ <img src="https://raw.githubusercontent.com/specbook-dev/specbook/main/.github/screenshot-pair.png" alt="Specbook with the Petstore spec open on a laptop and on a phone" width="840" />
30
+
31
+ See the [Specbook docs](https://docs.specbook.dev) for what else it can do: requests with checks, flows, tests that run in CI, and a portal for QA.
32
+
33
+ The package only returns an HTML page that loads the viewer, so it adds nothing to your app but one route.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install specbook-fastapi
39
+ # or
40
+ uv add specbook-fastapi
41
+ ```
42
+
43
+ Python 3.10 or newer, and FastAPI.
44
+
45
+ ## Usage
46
+
47
+ Serve the viewer at `/specbook` for the app's own OpenAPI document:
48
+
49
+ ```python
50
+ from fastapi import FastAPI
51
+ from specbook_fastapi import add_specbook
52
+
53
+ app = FastAPI()
54
+ add_specbook(app) # the viewer is at /specbook
55
+ ```
56
+
57
+ To use it instead of FastAPI's own docs page, turn that off and take its route:
58
+
59
+ ```python
60
+ app = FastAPI(docs_url=None, redoc_url=None)
61
+ add_specbook(app, route="/docs")
62
+ ```
63
+
64
+ The document's address and the page title come from the app (`app.openapi_url`, with the request's `root_path` in front of it, so it works behind a prefix or a proxy, and `app.title`). The route is left out of the OpenAPI document.
65
+
66
+ ### Your own route
67
+
68
+ `get_specbook_html` returns an `HTMLResponse`, for when you want to set the route up yourself:
69
+
70
+ ```python
71
+ from fastapi import FastAPI
72
+ from specbook_fastapi import get_specbook_html
73
+
74
+ app = FastAPI(docs_url=None)
75
+
76
+
77
+ @app.get("/docs", include_in_schema=False)
78
+ async def docs():
79
+ return get_specbook_html(openapi_url=app.openapi_url, title=f"{app.title} docs", default_env="live")
80
+ ```
81
+
82
+ ## Options
83
+
84
+ `add_specbook` takes `route` and `include_in_schema`, and passes every other keyword argument on to `get_specbook_html`.
85
+
86
+ | Option | Type | Default | Description |
87
+ | ------------- | ----------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
88
+ | `openapi_url` | `str` | `/openapi.json` | The OpenAPI document. A relative address is resolved against the page. `add_specbook` takes it from the app. |
89
+ | `title` | `str` | `API docs` | The page title. It is HTML-escaped. `add_specbook` takes it from the app. |
90
+ | `default_env` | `str` | `mock` | The environment shown first: `mock`, `live`, or the name of a server in the document. |
91
+ | `base_url` | `str` | the page's own origin | Where live requests go when the document lists no server. |
92
+ | `cdn` | `"specbook"` \| `"npm"` | `"specbook"` | Where the viewer is loaded from: the stable `v1` channel at `ui.specbook.dev`, or `@specbook/ui` on jsDelivr. |
93
+ | `version` | `str` | `v1` / tested release | With `specbook`, the channel. With `npm`, a version or range of `@specbook/ui`. The default is the release this was tested with. |
94
+ | `host` | `str` | the host of `cdn` | The host, for example `https://unpkg.com` or an internal mirror. |
95
+ | `src` | `str` | | The full address of `specbook.js`, for a copy you serve yourself. It overrides `cdn`, `version` and `host`. |
96
+ | `nonce` | `str` | | A [CSP nonce](https://developer.mozilla.org/docs/Web/HTTP/CSP#nonces) for the viewer's script. |
97
+
98
+ The viewer's own settings (`default_env`, the page title, the Content-Security-Policy it needs) are described in the [embed reference](https://docs.specbook.dev/reference/embed/).
99
+
100
+ The `specbook` host is the newest deployed build of the `v1` line, so fixes reach your app without a new release of this package. A breaking change to the viewer would ship as `/v2/`. The `npm` host is an immutable release of `@specbook/ui`; pin `version` to hold it still.
101
+
102
+ ## Content-Security-Policy
103
+
104
+ The page has no inline script. The JSON that configures the viewer is data, so it needs no nonce, and the one module script takes `nonce` when your policy uses nonces.
105
+
106
+ | Where the viewer comes from | The policy needs |
107
+ | --------------------------- | ------------------------------------------------------------------------------------- |
108
+ | A copy you serve (`src`) | `script-src 'self'` and `worker-src 'self'` |
109
+ | `ui.specbook.dev` | that host in `script-src`, `style-src` and `worker-src`, plus `blob:` in `worker-src` |
110
+ | jsDelivr (`cdn="npm"`) | `https://cdn.jsdelivr.net` in the same three directives, plus `blob:` in `worker-src` |
111
+
112
+ ## Privacy
113
+
114
+ The viewer runs no analytics or tracking script, and your document and your API's responses stay in the visitor's browser; nothing is sent to Specbook. Loading the script from `ui.specbook.dev` or jsDelivr means that host sees the visitor's IP address and the file requests, as any CDN does; [serve the viewer yourself](#serving-the-viewer-yourself) to avoid that.
115
+
116
+ The overview shows a small "Powered by Specbook" link. It points to `docs.specbook.dev` with your site's hostname in the address, and the Specbook docs site uses Cloudflare Web Analytics.
117
+
118
+ ## Serving the viewer yourself
119
+
120
+ Every Specbook release carries the viewer's files as `specbook-viewer-<version>.zip` (MIT, with its third-party licence notices) on its [GitHub release](https://github.com/specbook-dev/specbook/releases), and `@specbook/ui` on npm ships the same files in `dist-viewer/`. Unpack them into a folder, serve it from your app and point `src` at it, so nothing is loaded from another host:
121
+
122
+ ```python
123
+ from fastapi import FastAPI
124
+ from fastapi.staticfiles import StaticFiles
125
+ from specbook_fastapi import add_specbook
126
+
127
+ app = FastAPI(docs_url=None, redoc_url=None)
128
+ app.mount("/specbook-viewer", StaticFiles(directory="specbook-viewer"), name="specbook-viewer")
129
+ add_specbook(app, route="/docs", src="/specbook-viewer/specbook.js")
130
+ ```
131
+
132
+ ## Author
133
+
134
+ Maintained by the [Specbook](https://github.com/specbook-dev/specbook) team. Issues are welcome at [specbook-dev/specbook](https://github.com/specbook-dev/specbook/issues).
135
+
136
+ ## License
137
+
138
+ MIT
@@ -0,0 +1,7 @@
1
+ specbook_fastapi/__init__.py,sha256=GGCa3BG4qg-J2rfuEDSArdw_VszQJPRubh_Uk4A6rH8,3879
2
+ specbook_fastapi/_html.py,sha256=dn-5ncMOfx2G2PRfT-P6Yprti5MJQuqxGU0TRq4zXXM,2868
3
+ specbook_fastapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ specbook_fastapi-0.1.0.dist-info/METADATA,sha256=3m3vQPHVHAYxfNwnfvySYfBow-nJs8TkUwmio1citXU,8072
5
+ specbook_fastapi-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
6
+ specbook_fastapi-0.1.0.dist-info/licenses/LICENSE,sha256=mAuNVlLgsfI3K4qHktqIH-5z6nEPT1k02xpMjCNf7KI,1069
7
+ specbook_fastapi-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ajai Shankar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.