|
|
|
@ -55,6 +55,7 @@ error_logger = logging.getLogger("iti.error")
|
|
|
|
_current_request: ContextVar[Request | None] = ContextVar("iti_current_request", default=None)
|
|
|
|
_current_request: ContextVar[Request | None] = ContextVar("iti_current_request", default=None)
|
|
|
|
DOCS_PICKER_TEMPLATE = "docs-picker.html"
|
|
|
|
DOCS_PICKER_TEMPLATE = "docs-picker.html"
|
|
|
|
SCALAR_TEMPLATE = "scalar.html"
|
|
|
|
SCALAR_TEMPLATE = "scalar.html"
|
|
|
|
|
|
|
|
OPENAPI_HTTP_METHODS = {"get", "put", "post", "delete", "options", "head", "patch", "trace"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_app(
|
|
|
|
def create_app(
|
|
|
|
@ -91,6 +92,7 @@ def create_app(
|
|
|
|
redoc_url=None,
|
|
|
|
redoc_url=None,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
install_docs(app)
|
|
|
|
install_docs(app)
|
|
|
|
|
|
|
|
install_openapi_tag_groups(app)
|
|
|
|
app.state.config = config
|
|
|
|
app.state.config = config
|
|
|
|
app.state.cache = CacheManager(default_timeout=config.cache_default_timeout)
|
|
|
|
app.state.cache = CacheManager(default_timeout=config.cache_default_timeout)
|
|
|
|
app.state.limiter = SimpleLimiter(enabled=config.ratelimit_enabled)
|
|
|
|
app.state.limiter = SimpleLimiter(enabled=config.ratelimit_enabled)
|
|
|
|
@ -158,6 +160,17 @@ def install_docs(app: FastAPI) -> None:
|
|
|
|
return _docs_picker_html(app, doc_options)
|
|
|
|
return _docs_picker_html(app, doc_options)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def install_openapi_tag_groups(app: FastAPI) -> None:
|
|
|
|
|
|
|
|
default_openapi = app.openapi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def openapi_with_tag_groups() -> dict[str, Any]:
|
|
|
|
|
|
|
|
schema = default_openapi()
|
|
|
|
|
|
|
|
_apply_openapi_tag_groups(schema)
|
|
|
|
|
|
|
|
return schema
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.openapi = openapi_with_tag_groups
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _enabled_doc_options(app: FastAPI) -> dict[str, dict[str, str]]:
|
|
|
|
def _enabled_doc_options(app: FastAPI) -> dict[str, dict[str, str]]:
|
|
|
|
configured = getattr(app.state.config, "docs_ui_enabled", ["swagger", "scalar", "redoc"])
|
|
|
|
configured = getattr(app.state.config, "docs_ui_enabled", ["swagger", "scalar", "redoc"])
|
|
|
|
all_options = {
|
|
|
|
all_options = {
|
|
|
|
@ -237,6 +250,84 @@ def _render_template(name: str, values: Mapping[str, str]) -> str:
|
|
|
|
return template
|
|
|
|
return template
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _apply_openapi_tag_groups(schema: dict[str, Any]) -> None:
|
|
|
|
|
|
|
|
tag_names = _openapi_tag_names(schema)
|
|
|
|
|
|
|
|
if not tag_names or not any(_openapi_tag_display_name(tag) for tag in tag_names):
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
schema["tags"] = _openapi_tag_objects(schema, tag_names)
|
|
|
|
|
|
|
|
groups: list[dict[str, Any]] = []
|
|
|
|
|
|
|
|
group_index: dict[str, dict[str, Any]] = {}
|
|
|
|
|
|
|
|
for tag in tag_names:
|
|
|
|
|
|
|
|
group_name = _openapi_tag_group_name(tag)
|
|
|
|
|
|
|
|
group = group_index.get(group_name)
|
|
|
|
|
|
|
|
if group is None:
|
|
|
|
|
|
|
|
group = {"name": group_name, "tags": []}
|
|
|
|
|
|
|
|
groups.append(group)
|
|
|
|
|
|
|
|
group_index[group_name] = group
|
|
|
|
|
|
|
|
group["tags"].append(tag)
|
|
|
|
|
|
|
|
schema["x-tagGroups"] = groups
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _openapi_tag_names(schema: Mapping[str, Any]) -> list[str]:
|
|
|
|
|
|
|
|
names: list[str] = []
|
|
|
|
|
|
|
|
seen: set[str] = set()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def append_tag(value: Any) -> None:
|
|
|
|
|
|
|
|
if isinstance(value, str) and value not in seen:
|
|
|
|
|
|
|
|
names.append(value)
|
|
|
|
|
|
|
|
seen.add(value)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for tag in schema.get("tags") or []:
|
|
|
|
|
|
|
|
if isinstance(tag, Mapping):
|
|
|
|
|
|
|
|
append_tag(tag.get("name"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
paths = schema.get("paths") or {}
|
|
|
|
|
|
|
|
if not isinstance(paths, Mapping):
|
|
|
|
|
|
|
|
return names
|
|
|
|
|
|
|
|
for path_item in paths.values():
|
|
|
|
|
|
|
|
if not isinstance(path_item, Mapping):
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
for method, operation in path_item.items():
|
|
|
|
|
|
|
|
if method.lower() not in OPENAPI_HTTP_METHODS or not isinstance(operation, Mapping):
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
for tag in operation.get("tags") or []:
|
|
|
|
|
|
|
|
append_tag(tag)
|
|
|
|
|
|
|
|
return names
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _openapi_tag_objects(schema: Mapping[str, Any], tag_names: list[str]) -> list[dict[str, Any]]:
|
|
|
|
|
|
|
|
existing: dict[str, dict[str, Any]] = {}
|
|
|
|
|
|
|
|
for tag in schema.get("tags") or []:
|
|
|
|
|
|
|
|
if not isinstance(tag, Mapping) or not isinstance(tag.get("name"), str):
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
existing.setdefault(tag["name"], dict(tag))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tag_objects: list[dict[str, Any]] = []
|
|
|
|
|
|
|
|
for tag_name in tag_names:
|
|
|
|
|
|
|
|
tag = dict(existing.get(tag_name, {"name": tag_name}))
|
|
|
|
|
|
|
|
tag["name"] = tag_name
|
|
|
|
|
|
|
|
display_name = _openapi_tag_display_name(tag_name)
|
|
|
|
|
|
|
|
if display_name and "x-displayName" not in tag:
|
|
|
|
|
|
|
|
tag["x-displayName"] = display_name
|
|
|
|
|
|
|
|
tag_objects.append(tag)
|
|
|
|
|
|
|
|
return tag_objects
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _openapi_tag_group_name(tag: str) -> str:
|
|
|
|
|
|
|
|
prefix, separator, suffix = tag.partition(".")
|
|
|
|
|
|
|
|
if separator and prefix and suffix:
|
|
|
|
|
|
|
|
return prefix
|
|
|
|
|
|
|
|
return tag
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _openapi_tag_display_name(tag: str) -> str | None:
|
|
|
|
|
|
|
|
prefix, separator, suffix = tag.partition(".")
|
|
|
|
|
|
|
|
if separator and prefix and suffix:
|
|
|
|
|
|
|
|
return suffix
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def init_middlewares(app: FastAPI) -> None:
|
|
|
|
def init_middlewares(app: FastAPI) -> None:
|
|
|
|
@app.middleware("http")
|
|
|
|
@app.middleware("http")
|
|
|
|
async def request_context_middleware(request: Request, call_next):
|
|
|
|
async def request_context_middleware(request: Request, call_next):
|
|
|
|
|