mirror of
https://github.com/langgenius/dify.git
synced 2026-05-10 05:56:31 +08:00
fastopenapi doc
This commit is contained in:
parent
9b0f4111b0
commit
e3259a4ec2
95
api/dev/generate_fastopenapi_specs.py
Normal file
95
api/dev/generate_fastopenapi_specs.py
Normal file
@ -0,0 +1,95 @@
|
||||
"""Generate FastOpenAPI OpenAPI 3.0 specs without booting the full backend."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
API_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(API_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(API_ROOT))
|
||||
|
||||
from dev.generate_swagger_specs import _apply_runtime_defaults, _drop_null_values, _sort_swagger_arrays
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FastOpenApiSpecTarget:
|
||||
route: str
|
||||
filename: str
|
||||
|
||||
|
||||
FASTOPENAPI_SPEC_TARGETS: tuple[FastOpenApiSpecTarget, ...] = (
|
||||
FastOpenApiSpecTarget(route="/fastopenapi/openapi.json", filename="fastopenapi-console-openapi.json"),
|
||||
)
|
||||
|
||||
|
||||
def create_fastopenapi_spec_app():
|
||||
"""Build a minimal Flask app that only mounts FastOpenAPI docs routes."""
|
||||
|
||||
_apply_runtime_defaults()
|
||||
|
||||
from app_factory import create_flask_app_with_configs
|
||||
from extensions import ext_fastopenapi
|
||||
|
||||
app = create_flask_app_with_configs()
|
||||
ext_fastopenapi.init_app(app)
|
||||
return app
|
||||
|
||||
|
||||
def generate_fastopenapi_specs(output_dir: Path) -> list[Path]:
|
||||
"""Write FastOpenAPI specs to `output_dir` and return the written paths."""
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
app = create_fastopenapi_spec_app()
|
||||
client = app.test_client()
|
||||
|
||||
written_paths: list[Path] = []
|
||||
for target in FASTOPENAPI_SPEC_TARGETS:
|
||||
response = client.get(target.route)
|
||||
if response.status_code != 200:
|
||||
raise RuntimeError(f"failed to fetch {target.route}: {response.status_code}")
|
||||
|
||||
payload = response.get_json()
|
||||
if not isinstance(payload, dict):
|
||||
raise RuntimeError(f"unexpected response payload for {target.route}")
|
||||
payload = _drop_null_values(payload)
|
||||
payload = _sort_swagger_arrays(payload)
|
||||
|
||||
output_path = output_dir / target.filename
|
||||
output_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
written_paths.append(output_path)
|
||||
|
||||
return written_paths
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output-dir",
|
||||
type=Path,
|
||||
default=Path("openapi"),
|
||||
help="Directory where the OpenAPI JSON files will be written.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
written_paths = generate_fastopenapi_specs(args.output_dir)
|
||||
|
||||
for path in written_paths:
|
||||
logger.debug(path)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@ -1,8 +1,8 @@
|
||||
"""Generate Swagger JSON specs and Markdown API docs.
|
||||
"""Generate OpenAPI JSON specs and split Markdown API docs.
|
||||
|
||||
The Markdown step uses `swagger-markdown`, the same converter family as the
|
||||
Swagger Markdown UI, so CI and local regeneration catch converter-incompatible
|
||||
Swagger output early.
|
||||
OpenAPI output early.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@ -12,49 +12,112 @@ import logging
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
API_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(API_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(API_ROOT))
|
||||
|
||||
from dev.generate_fastopenapi_specs import FASTOPENAPI_SPEC_TARGETS, generate_fastopenapi_specs
|
||||
from dev.generate_swagger_specs import SPEC_TARGETS, generate_specs
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SWAGGER_MARKDOWN_PACKAGE = "swagger-markdown@3.0.0"
|
||||
CONSOLE_SWAGGER_FILENAME = "console-swagger.json"
|
||||
STALE_COMBINED_MARKDOWN_FILENAME = "api-reference.md"
|
||||
|
||||
|
||||
def generate_markdown_docs(swagger_dir: Path, markdown_dir: Path, *, keep_swagger_json: bool = False) -> list[Path]:
|
||||
"""Generate Swagger JSON files, convert each one to Markdown, and return Markdown paths."""
|
||||
def _convert_spec_to_markdown(spec_path: Path, markdown_path: Path) -> None:
|
||||
subprocess.run(
|
||||
[
|
||||
"npx",
|
||||
"--yes",
|
||||
SWAGGER_MARKDOWN_PACKAGE,
|
||||
"-i",
|
||||
str(spec_path),
|
||||
"-o",
|
||||
str(markdown_path),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
def _demote_markdown_headings(markdown: str, *, levels: int = 1) -> str:
|
||||
"""Nest generated Markdown under another Markdown section."""
|
||||
|
||||
heading_prefix = "#" * levels
|
||||
lines = []
|
||||
for line in markdown.splitlines():
|
||||
if line.startswith("#"):
|
||||
lines.append(f"{heading_prefix}{line}")
|
||||
else:
|
||||
lines.append(line)
|
||||
return "\n".join(lines).strip()
|
||||
|
||||
|
||||
def _append_fastopenapi_markdown(console_markdown_path: Path, fastopenapi_markdown_path: Path) -> None:
|
||||
"""Append FastOpenAPI console docs to the existing console API Markdown."""
|
||||
|
||||
console_markdown = console_markdown_path.read_text(encoding="utf-8").rstrip()
|
||||
fastopenapi_markdown = _demote_markdown_headings(
|
||||
fastopenapi_markdown_path.read_text(encoding="utf-8"),
|
||||
levels=2,
|
||||
)
|
||||
console_markdown_path.write_text(
|
||||
"\n\n".join(
|
||||
[
|
||||
console_markdown,
|
||||
"## FastOpenAPI Preview (OpenAPI 3.0)",
|
||||
fastopenapi_markdown,
|
||||
]
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def generate_markdown_docs(
|
||||
swagger_dir: Path,
|
||||
markdown_dir: Path,
|
||||
*,
|
||||
keep_swagger_json: bool = False,
|
||||
) -> list[Path]:
|
||||
"""Generate intermediate specs, convert them to split Markdown API docs, and return Markdown paths."""
|
||||
|
||||
swagger_paths = generate_specs(swagger_dir)
|
||||
fastopenapi_paths = generate_fastopenapi_specs(swagger_dir)
|
||||
spec_paths = [*swagger_paths, *fastopenapi_paths]
|
||||
swagger_paths_by_name = {path.name: path for path in swagger_paths}
|
||||
fastopenapi_paths_by_name = {path.name: path for path in fastopenapi_paths}
|
||||
|
||||
markdown_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
written_paths: list[Path] = []
|
||||
try:
|
||||
for target in SPEC_TARGETS:
|
||||
swagger_path = swagger_paths_by_name[target.filename]
|
||||
markdown_path = markdown_dir / f"{swagger_path.stem}.md"
|
||||
subprocess.run(
|
||||
[
|
||||
"npx",
|
||||
"--yes",
|
||||
SWAGGER_MARKDOWN_PACKAGE,
|
||||
"-i",
|
||||
str(swagger_path),
|
||||
"-o",
|
||||
str(markdown_path),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
written_paths.append(markdown_path)
|
||||
with tempfile.TemporaryDirectory(prefix="dify-api-docs-") as temp_dir:
|
||||
temp_markdown_dir = Path(temp_dir)
|
||||
|
||||
for target in SPEC_TARGETS:
|
||||
swagger_path = swagger_paths_by_name[target.filename]
|
||||
markdown_path = markdown_dir / f"{swagger_path.stem}.md"
|
||||
_convert_spec_to_markdown(swagger_path, markdown_path)
|
||||
written_paths.append(markdown_path)
|
||||
|
||||
for target in FASTOPENAPI_SPEC_TARGETS:
|
||||
fastopenapi_path = fastopenapi_paths_by_name[target.filename]
|
||||
markdown_path = temp_markdown_dir / f"{fastopenapi_path.stem}.md"
|
||||
_convert_spec_to_markdown(fastopenapi_path, markdown_path)
|
||||
|
||||
console_markdown_path = markdown_dir / f"{Path(CONSOLE_SWAGGER_FILENAME).stem}.md"
|
||||
_append_fastopenapi_markdown(console_markdown_path, markdown_path)
|
||||
|
||||
(markdown_dir / STALE_COMBINED_MARKDOWN_FILENAME).unlink(missing_ok=True)
|
||||
finally:
|
||||
if not keep_swagger_json:
|
||||
if swagger_dir == markdown_dir or markdown_dir.is_relative_to(swagger_dir):
|
||||
for path in swagger_paths:
|
||||
for path in spec_paths:
|
||||
path.unlink(missing_ok=True)
|
||||
else:
|
||||
shutil.rmtree(swagger_dir, ignore_errors=True)
|
||||
@ -68,18 +131,18 @@ def parse_args() -> argparse.Namespace:
|
||||
"--swagger-dir",
|
||||
type=Path,
|
||||
default=Path("openapi"),
|
||||
help="Directory where Swagger JSON files will be written.",
|
||||
help="Directory where intermediate JSON spec files will be written.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--markdown-dir",
|
||||
type=Path,
|
||||
default=Path("openapi/markdown"),
|
||||
help="Directory where Markdown API docs will be written.",
|
||||
help="Directory where split Markdown API docs will be written.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--keep-swagger-json",
|
||||
action="store_true",
|
||||
help="Keep intermediate Swagger JSON files after Markdown generation.",
|
||||
help="Keep intermediate JSON spec files after Markdown generation.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@ -13277,7 +13277,7 @@ Default value types for form inputs.
|
||||
| app | | | No |
|
||||
| app_id | string | | Yes |
|
||||
| can_trial | | | No |
|
||||
| categories | [ string ] | | No |
|
||||
| category | | | No |
|
||||
| copyright | | | No |
|
||||
| custom_disclaimer | | | No |
|
||||
| description | | | No |
|
||||
@ -14598,3 +14598,169 @@ Workflow tool configuration
|
||||
| mentioned_user_account | [_AnonymousInlineModel_6fec07cd0d85](#_anonymousinlinemodel_6fec07cd0d85) | | No |
|
||||
| mentioned_user_id | string | | No |
|
||||
| reply_id | string | | No |
|
||||
|
||||
## FastOpenAPI Preview (OpenAPI 3.0)
|
||||
|
||||
### Dify API (FastOpenAPI PoC)
|
||||
FastOpenAPI proof of concept for Dify API
|
||||
|
||||
#### Version: 1.0
|
||||
|
||||
---
|
||||
|
||||
##### [GET] /console/api/init
|
||||
**Get initialization validation status.**
|
||||
|
||||
###### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | OK | **application/json**: [InitStatusResponse](#initstatusresponse)<br> |
|
||||
|
||||
##### [POST] /console/api/init
|
||||
**Validate initialization password.**
|
||||
|
||||
###### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [InitValidatePayload](#initvalidatepayload)<br> |
|
||||
|
||||
###### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 201 | Created | **application/json**: [InitValidateResponse](#initvalidateresponse)<br> |
|
||||
|
||||
##### [GET] /console/api/ping
|
||||
**Health check endpoint for connection testing.**
|
||||
|
||||
###### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | OK | **application/json**: [PingResponse](#pingresponse)<br> |
|
||||
|
||||
##### [GET] /console/api/setup
|
||||
**Get system setup status.
|
||||
|
||||
NOTE: This endpoint is unauthenticated by design.
|
||||
|
||||
During first-time bootstrap there is no admin account yet, so frontend initialization must be
|
||||
able to query setup progress before any login flow exists.
|
||||
|
||||
Only bootstrap-safe status information should be returned by this endpoint.
|
||||
**
|
||||
|
||||
###### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | OK | **application/json**: [SetupStatusResponse](#setupstatusresponse)<br> |
|
||||
|
||||
##### [POST] /console/api/setup
|
||||
**Initialize system setup with admin account.
|
||||
|
||||
NOTE: This endpoint is unauthenticated by design for first-time bootstrap.
|
||||
Access is restricted by deployment mode (`SELF_HOSTED`), one-time setup guards,
|
||||
and init-password validation rather than user session authentication.
|
||||
**
|
||||
|
||||
###### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [SetupRequestPayload](#setuprequestpayload)<br> |
|
||||
|
||||
###### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 201 | Created | **application/json**: [SetupResponse](#setupresponse)<br> |
|
||||
|
||||
##### [GET] /console/api/version
|
||||
**Check for application version updates.**
|
||||
|
||||
###### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| current_version | query | | Yes | string |
|
||||
|
||||
###### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | OK | **application/json**: [VersionResponse](#versionresponse)<br> |
|
||||
|
||||
---
|
||||
##### Schemas
|
||||
|
||||
###### ErrorSchema
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| error | { **"details"**: string, **"message"**: string, **"status"**: integer, **"type"**: string } | | Yes |
|
||||
|
||||
###### InitStatusResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| status | string, <br>**Available values:** "finished", "not_started" | Initialization status<br>*Enum:* `"finished"`, `"not_started"` | Yes |
|
||||
|
||||
###### InitValidatePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| password | string | Initialization password | Yes |
|
||||
|
||||
###### InitValidateResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| result | string | Operation result | Yes |
|
||||
|
||||
###### PingResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| result | string | Health check result | Yes |
|
||||
|
||||
###### SetupRequestPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| email | string | Admin email address | Yes |
|
||||
| language | | Admin language | No |
|
||||
| name | string | Admin name (max 30 characters) | Yes |
|
||||
| password | string | Admin password | Yes |
|
||||
|
||||
###### SetupResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| result | string | Setup result | Yes |
|
||||
|
||||
###### SetupStatusResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| setup_at | | Setup completion time (ISO format) | No |
|
||||
| step | string, <br>**Available values:** "finished", "not_started" | Setup step status<br>*Enum:* `"finished"`, `"not_started"` | Yes |
|
||||
|
||||
###### VersionFeatures
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| can_replace_logo | boolean | Whether logo replacement is supported | Yes |
|
||||
| model_load_balancing_enabled | boolean | Whether model load balancing is enabled | Yes |
|
||||
|
||||
###### VersionResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| can_auto_update | boolean | Whether auto-update is supported | Yes |
|
||||
| features | [VersionFeatures](#versionfeatures) | Feature flags and capabilities | Yes |
|
||||
| release_date | string | Release date of latest version | Yes |
|
||||
| release_notes | string | Release notes for latest version | Yes |
|
||||
| version | string | Latest version number | Yes |
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
"""Unit tests for the Markdown API docs generator."""
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _load_generate_swagger_markdown_docs_module():
|
||||
api_dir = Path(__file__).resolve().parents[3]
|
||||
script_path = api_dir / "dev" / "generate_swagger_markdown_docs.py"
|
||||
|
||||
spec = importlib.util.spec_from_file_location("generate_swagger_markdown_docs", script_path)
|
||||
assert spec
|
||||
assert spec.loader
|
||||
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module) # type: ignore[attr-defined]
|
||||
return module
|
||||
|
||||
|
||||
def test_generate_markdown_docs_keeps_split_docs_and_merges_fastopenapi_into_console(tmp_path, monkeypatch):
|
||||
module = _load_generate_swagger_markdown_docs_module()
|
||||
swagger_dir = tmp_path / "openapi"
|
||||
markdown_dir = tmp_path / "markdown"
|
||||
stale_combined_doc = markdown_dir / "api-reference.md"
|
||||
markdown_dir.mkdir()
|
||||
stale_combined_doc.write_text("stale", encoding="utf-8")
|
||||
|
||||
def write_specs(output_dir: Path) -> list[Path]:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
paths = []
|
||||
for target in module.SPEC_TARGETS:
|
||||
path = output_dir / target.filename
|
||||
path.write_text("{}", encoding="utf-8")
|
||||
paths.append(path)
|
||||
return paths
|
||||
|
||||
def write_fastopenapi_specs(output_dir: Path) -> list[Path]:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
path = output_dir / module.FASTOPENAPI_SPEC_TARGETS[0].filename
|
||||
path.write_text("{}", encoding="utf-8")
|
||||
return [path]
|
||||
|
||||
def convert_spec_to_markdown(spec_path: Path, markdown_path: Path) -> None:
|
||||
markdown_path.write_text(f"# {spec_path.stem}\n\n## Routes\n", encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(module, "generate_specs", write_specs)
|
||||
monkeypatch.setattr(module, "generate_fastopenapi_specs", write_fastopenapi_specs)
|
||||
monkeypatch.setattr(module, "_convert_spec_to_markdown", convert_spec_to_markdown)
|
||||
|
||||
written_paths = module.generate_markdown_docs(swagger_dir, markdown_dir)
|
||||
|
||||
assert [path.name for path in written_paths] == [
|
||||
"console-swagger.md",
|
||||
"web-swagger.md",
|
||||
"service-swagger.md",
|
||||
]
|
||||
assert not stale_combined_doc.exists()
|
||||
assert not list(swagger_dir.glob("*.json"))
|
||||
|
||||
console_markdown = (markdown_dir / "console-swagger.md").read_text(encoding="utf-8")
|
||||
assert "## FastOpenAPI Preview (OpenAPI 3.0)" in console_markdown
|
||||
assert "### fastopenapi-console-openapi" in console_markdown
|
||||
assert "#### Routes" in console_markdown
|
||||
assert "FastOpenAPI Preview" not in (markdown_dir / "web-swagger.md").read_text(encoding="utf-8")
|
||||
assert "FastOpenAPI Preview" not in (markdown_dir / "service-swagger.md").read_text(encoding="utf-8")
|
||||
Loading…
Reference in New Issue
Block a user