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,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.
|