mirror of
https://github.com/langgenius/dify.git
synced 2026-09-24 23:22:26 +08:00
chore: update to openapi v3 by change dep (#37316)
Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Co-authored-by: Stephen Zhou <hi@hyoban.cc> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Stephen Zhou
Stephen Zhou
autofix-ci[bot]
parent
9c25fa1c96
commit
6c0cce4b7f
@@ -1,9 +1,9 @@
|
||||
"""Helpers for registering Pydantic models with Flask-RESTX namespaces.
|
||||
|
||||
Flask-RESTX treats `SchemaModel` bodies as opaque JSON schemas; it does not
|
||||
promote Pydantic's nested `$defs` into top-level Swagger `definitions`.
|
||||
promote Pydantic's nested `$defs` into top-level OpenAPI component schemas.
|
||||
These helpers keep that translation centralized so models registered through
|
||||
`register_schema_models` emit resolvable Swagger 2.0 references.
|
||||
`register_schema_models` emit resolvable OpenAPI 3 references.
|
||||
"""
|
||||
|
||||
from collections.abc import Iterable, Mapping
|
||||
@@ -14,7 +14,7 @@ from flask import request
|
||||
from flask_restx import Namespace
|
||||
from pydantic import BaseModel, TypeAdapter
|
||||
|
||||
DEFAULT_REF_TEMPLATE_SWAGGER_2_0 = "#/definitions/{model}"
|
||||
DEFAULT_REF_TEMPLATE_OPENAPI_3_0 = "#/components/schemas/{model}"
|
||||
|
||||
|
||||
QueryParamDoc = TypedDict(
|
||||
@@ -48,7 +48,6 @@ class QueryArgs(Protocol):
|
||||
def _register_json_schema(namespace: Namespace, name: str, schema: dict) -> None:
|
||||
"""Register a JSON schema and promote any nested Pydantic `$defs`."""
|
||||
|
||||
schema = _swagger_2_compatible_schema(schema)
|
||||
nested_definitions = schema.get("$defs")
|
||||
schema_to_register = dict(schema)
|
||||
if isinstance(nested_definitions, dict):
|
||||
@@ -71,41 +70,12 @@ def _register_schema_model(namespace: Namespace, model: type[BaseModel], *, mode
|
||||
_register_json_schema(
|
||||
namespace,
|
||||
model.__name__,
|
||||
model.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0, mode=mode),
|
||||
model.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0, mode=mode),
|
||||
)
|
||||
|
||||
|
||||
def _swagger_2_compatible_schema(value: Any) -> Any:
|
||||
if isinstance(value, list):
|
||||
return [_swagger_2_compatible_schema(item) for item in value]
|
||||
|
||||
if not isinstance(value, dict):
|
||||
return value
|
||||
|
||||
converted = {key: _swagger_2_compatible_schema(child) for key, child in value.items()}
|
||||
any_of = value.get("anyOf")
|
||||
if not isinstance(any_of, list):
|
||||
return converted
|
||||
|
||||
non_null_candidates = [
|
||||
candidate for candidate in any_of if isinstance(candidate, Mapping) and candidate.get("type") != "null"
|
||||
]
|
||||
has_null_candidate = any(isinstance(candidate, Mapping) and candidate.get("type") == "null" for candidate in any_of)
|
||||
if not has_null_candidate or len(non_null_candidates) != 1:
|
||||
return converted
|
||||
|
||||
non_null_schema = _swagger_2_compatible_schema(dict(non_null_candidates[0]))
|
||||
if not isinstance(non_null_schema, dict):
|
||||
return converted
|
||||
|
||||
converted.pop("anyOf", None)
|
||||
converted.update(non_null_schema)
|
||||
converted["x-nullable"] = True
|
||||
return converted
|
||||
|
||||
|
||||
def register_schema_model(namespace: Namespace, model: type[BaseModel]) -> None:
|
||||
"""Register a BaseModel and its nested schema definitions for Swagger documentation."""
|
||||
"""Register a BaseModel and its nested component schemas for OpenAPI documentation."""
|
||||
|
||||
_register_schema_model(namespace, model, mode="validation")
|
||||
|
||||
@@ -146,7 +116,7 @@ def register_enum_models(namespace: Namespace, *models: type[StrEnum]) -> None:
|
||||
_register_json_schema(
|
||||
namespace,
|
||||
model.__name__,
|
||||
TypeAdapter(model).json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0),
|
||||
TypeAdapter(model).json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0),
|
||||
)
|
||||
|
||||
|
||||
@@ -155,10 +125,10 @@ def query_params_from_model(model: type[BaseModel]) -> dict[str, QueryParamDoc]:
|
||||
|
||||
`Namespace.expect()` treats Pydantic schema models as request bodies, so GET
|
||||
endpoints should keep runtime validation on the Pydantic model and feed this
|
||||
derived mapping to `Namespace.doc(params=...)` for Swagger documentation.
|
||||
derived mapping to `Namespace.doc(params=...)` for OpenAPI documentation.
|
||||
"""
|
||||
|
||||
schema = model.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0)
|
||||
schema = model.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0)
|
||||
properties = schema.get("properties", {})
|
||||
if not isinstance(properties, Mapping):
|
||||
return {}
|
||||
@@ -203,7 +173,7 @@ def query_params_from_request[ModelT: BaseModel](
|
||||
|
||||
|
||||
def _drop_malformed_defaulted_integer_params(model: type[BaseModel], params: dict[str, Any]) -> None:
|
||||
properties = model.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0).get("properties", {})
|
||||
properties = model.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0).get("properties", {})
|
||||
if not isinstance(properties, Mapping):
|
||||
return
|
||||
|
||||
@@ -297,7 +267,7 @@ def _nullable_property_schema(property_schema: Mapping[str, Any]) -> Mapping[str
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_REF_TEMPLATE_SWAGGER_2_0",
|
||||
"DEFAULT_REF_TEMPLATE_OPENAPI_3_0",
|
||||
"get_or_create_model",
|
||||
"query_params_from_model",
|
||||
"query_params_from_request",
|
||||
|
||||
@@ -2,6 +2,7 @@ from flask import request
|
||||
from flask_restx import Resource, fields
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from controllers.common.schema import DEFAULT_REF_TEMPLATE_OPENAPI_3_0
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from libs.login import login_required
|
||||
@@ -17,7 +18,7 @@ class AdvancedPromptTemplateQuery(BaseModel):
|
||||
|
||||
console_ns.schema_model(
|
||||
AdvancedPromptTemplateQuery.__name__,
|
||||
AdvancedPromptTemplateQuery.model_json_schema(ref_template="#/definitions/{model}"),
|
||||
AdvancedPromptTemplateQuery.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from libs.login import login_required
|
||||
from models import Account
|
||||
from services.billing_service import BillingService
|
||||
|
||||
from ...common.schema import DEFAULT_REF_TEMPLATE_OPENAPI_3_0
|
||||
from .. import console_ns
|
||||
from ..wraps import (
|
||||
account_initialization_required,
|
||||
@@ -23,7 +24,7 @@ class ComplianceDownloadQuery(BaseModel):
|
||||
|
||||
console_ns.schema_model(
|
||||
ComplianceDownloadQuery.__name__,
|
||||
ComplianceDownloadQuery.model_json_schema(ref_template="#/definitions/{model}"),
|
||||
ComplianceDownloadQuery.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from models.api_based_extension import APIBasedExtension
|
||||
from services.api_based_extension_service import APIBasedExtensionService
|
||||
from services.code_based_extension_service import CodeBasedExtensionService
|
||||
|
||||
from ..common.schema import DEFAULT_REF_TEMPLATE_SWAGGER_2_0, register_schema_models
|
||||
from ..common.schema import DEFAULT_REF_TEMPLATE_OPENAPI_3_0, register_schema_models
|
||||
from . import console_ns
|
||||
from .wraps import account_initialization_required, setup_required, with_current_tenant_id
|
||||
|
||||
@@ -63,7 +63,7 @@ class APIBasedExtensionResponse(ResponseModel):
|
||||
register_schema_models(console_ns, APIBasedExtensionPayload, CodeBasedExtensionResponse, APIBasedExtensionResponse)
|
||||
console_ns.schema_model(
|
||||
"APIBasedExtensionListResponse",
|
||||
TypeAdapter(list[APIBasedExtensionResponse]).json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0),
|
||||
TypeAdapter(list[APIBasedExtensionResponse]).json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Generate FastOpenAPI OpenAPI 3.0 specs without booting the full backend."""
|
||||
"""Generate FastOpenAPI OpenAPI 3.1 specs without booting the full backend."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""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
|
||||
legacy Markdown UI, so CI and local regeneration catch converter-incompatible
|
||||
OpenAPI output early.
|
||||
"""
|
||||
|
||||
@@ -25,19 +25,21 @@ 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"
|
||||
CONSOLE_OPENAPI_FILENAME = "console-openapi.json"
|
||||
STALE_COMBINED_MARKDOWN_FILENAME = "api-reference.md"
|
||||
|
||||
|
||||
def _definition_ref_name(schema: object) -> str | None:
|
||||
def _schema_ref_name(schema: object) -> str | None:
|
||||
if not isinstance(schema, dict):
|
||||
return None
|
||||
|
||||
ref = schema.get("$ref")
|
||||
if not isinstance(ref, str) or not ref.startswith("#/definitions/"):
|
||||
if not isinstance(ref, str):
|
||||
return None
|
||||
|
||||
return ref.removeprefix("#/definitions/")
|
||||
if ref.startswith("#/components/schemas/"):
|
||||
return ref.removeprefix("#/components/schemas/")
|
||||
return None
|
||||
|
||||
|
||||
def _markdown_anchor(name: str) -> str:
|
||||
@@ -48,7 +50,7 @@ def _schema_markdown_type(schema: object) -> str:
|
||||
if not isinstance(schema, dict):
|
||||
return ""
|
||||
|
||||
ref_name = _definition_ref_name(schema)
|
||||
ref_name = _schema_ref_name(schema)
|
||||
if ref_name is not None:
|
||||
return f"[{ref_name}](#{_markdown_anchor(ref_name)})"
|
||||
|
||||
@@ -111,15 +113,16 @@ def _has_union_schema(schema: object) -> bool:
|
||||
|
||||
|
||||
def _patch_union_schema_markdown(markdown: str, spec_path: Path) -> str:
|
||||
"""Fill Swagger Markdown table cells that `swagger-markdown` leaves blank for union schemas."""
|
||||
"""Fill Markdown table cells that `swagger-markdown` leaves blank for union schemas."""
|
||||
|
||||
spec = json.loads(spec_path.read_text(encoding="utf-8"))
|
||||
definitions = spec.get("definitions")
|
||||
if not isinstance(definitions, dict):
|
||||
components = spec.get("components")
|
||||
schemas = components.get("schemas") if isinstance(components, dict) else None
|
||||
if not isinstance(schemas, dict):
|
||||
return markdown
|
||||
|
||||
for definition_name, schema in definitions.items():
|
||||
if not isinstance(definition_name, str) or not isinstance(schema, dict):
|
||||
for schema_name, schema in schemas.items():
|
||||
if not isinstance(schema_name, str) or not isinstance(schema, dict):
|
||||
continue
|
||||
|
||||
properties = schema.get("properties")
|
||||
@@ -128,7 +131,7 @@ def _patch_union_schema_markdown(markdown: str, spec_path: Path) -> str:
|
||||
if isinstance(property_name, str) and _has_union_schema(property_schema):
|
||||
markdown = _replace_schema_table_type(
|
||||
markdown,
|
||||
definition_name,
|
||||
schema_name,
|
||||
property_name,
|
||||
_schema_markdown_type(property_schema),
|
||||
)
|
||||
@@ -139,14 +142,14 @@ def _patch_union_schema_markdown(markdown: str, spec_path: Path) -> str:
|
||||
|
||||
markdown = _replace_schema_table_type(
|
||||
markdown,
|
||||
definition_name,
|
||||
definition_name,
|
||||
schema_name,
|
||||
schema_name,
|
||||
_schema_markdown_type(schema),
|
||||
)
|
||||
|
||||
for variant in union_variants:
|
||||
variant_name = _definition_ref_name(variant)
|
||||
variant_schema = definitions.get(variant_name) if variant_name is not None else None
|
||||
variant_name = _schema_ref_name(variant)
|
||||
variant_schema = schemas.get(variant_name) if variant_name is not None else None
|
||||
if not isinstance(variant_name, str) or not isinstance(variant_schema, dict):
|
||||
continue
|
||||
properties = variant_schema.get("properties")
|
||||
@@ -229,7 +232,7 @@ def _append_fastopenapi_markdown(console_markdown_path: Path, fastopenapi_markdo
|
||||
"\n\n".join(
|
||||
[
|
||||
console_markdown,
|
||||
"## FastOpenAPI Preview (OpenAPI 3.0)",
|
||||
"## FastOpenAPI Preview (OpenAPI 3.1)",
|
||||
fastopenapi_markdown,
|
||||
]
|
||||
)
|
||||
@@ -239,17 +242,17 @@ def _append_fastopenapi_markdown(console_markdown_path: Path, fastopenapi_markdo
|
||||
|
||||
|
||||
def generate_markdown_docs(
|
||||
swagger_dir: Path,
|
||||
openapi_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}
|
||||
openapi_paths = generate_specs(openapi_dir)
|
||||
fastopenapi_paths = generate_fastopenapi_specs(openapi_dir)
|
||||
spec_paths = [*openapi_paths, *fastopenapi_paths]
|
||||
openapi_paths_by_name = {path.name: path for path in openapi_paths}
|
||||
fastopenapi_paths_by_name = {path.name: path for path in fastopenapi_paths}
|
||||
|
||||
markdown_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -260,9 +263,9 @@ def generate_markdown_docs(
|
||||
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)
|
||||
openapi_path = openapi_paths_by_name[target.filename]
|
||||
markdown_path = markdown_dir / f"{openapi_path.stem}.md"
|
||||
_convert_spec_to_markdown(openapi_path, markdown_path)
|
||||
written_paths.append(markdown_path)
|
||||
|
||||
for target in FASTOPENAPI_SPEC_TARGETS: # type: ignore
|
||||
@@ -270,7 +273,7 @@ def generate_markdown_docs(
|
||||
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"
|
||||
console_markdown_path = markdown_dir / f"{Path(CONSOLE_OPENAPI_FILENAME).stem}.md"
|
||||
_append_fastopenapi_markdown(console_markdown_path, markdown_path)
|
||||
|
||||
(markdown_dir / STALE_COMBINED_MARKDOWN_FILENAME).unlink(missing_ok=True)
|
||||
@@ -286,6 +289,8 @@ def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--swagger-dir",
|
||||
"--openapi-dir",
|
||||
dest="openapi_dir",
|
||||
type=Path,
|
||||
default=Path("openapi"),
|
||||
help="Directory where intermediate JSON spec files will be written.",
|
||||
@@ -307,7 +312,7 @@ def parse_args() -> argparse.Namespace:
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
written_paths = generate_markdown_docs(
|
||||
args.swagger_dir,
|
||||
args.openapi_dir,
|
||||
args.markdown_dir,
|
||||
keep_swagger_json=args.keep_swagger_json,
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Generate Flask-RESTX Swagger 2.0 specs without booting the full backend.
|
||||
"""Generate Flask-RESTX OpenAPI 3 specs without booting the full backend.
|
||||
|
||||
This helper intentionally avoids `app_factory.create_app()`. The normal backend
|
||||
startup eagerly initializes database, Redis, Celery, and storage extensions,
|
||||
which is unnecessary when the goal is only to serialize the Flask-RESTX
|
||||
`/swagger.json` documents.
|
||||
`/openapi.json` documents.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -42,10 +42,10 @@ class RestxApi(Protocol):
|
||||
|
||||
|
||||
SPEC_TARGETS: tuple[SpecTarget, ...] = (
|
||||
SpecTarget(route="/console/api/swagger.json", filename="console-swagger.json", namespace="console"),
|
||||
SpecTarget(route="/api/swagger.json", filename="web-swagger.json", namespace="web"),
|
||||
SpecTarget(route="/v1/swagger.json", filename="service-swagger.json", namespace="service"),
|
||||
SpecTarget(route="/openapi/v1/swagger.json", filename="openapi-swagger.json", namespace="openapi"),
|
||||
SpecTarget(route="/console/api/openapi.json", filename="console-openapi.json", namespace="console"),
|
||||
SpecTarget(route="/api/openapi.json", filename="web-openapi.json", namespace="web"),
|
||||
SpecTarget(route="/v1/openapi.json", filename="service-openapi.json", namespace="service"),
|
||||
SpecTarget(route="/openapi/v1/openapi.json", filename="openapi-openapi.json", namespace="openapi"),
|
||||
)
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ def _inline_model_signature(nested_fields: dict[object, object]) -> object:
|
||||
|
||||
|
||||
def _inline_model_name(nested_fields: dict[object, object]) -> str:
|
||||
"""Return a stable Swagger model name for an anonymous inline field map."""
|
||||
"""Return a stable OpenAPI model name for an anonymous inline field map."""
|
||||
|
||||
signature = json.dumps(_inline_model_signature(nested_fields), sort_keys=True, separators=(",", ":"))
|
||||
digest = hashlib.sha1(signature.encode("utf-8")).hexdigest()[:12]
|
||||
@@ -134,7 +134,7 @@ def _inline_model_name(nested_fields: dict[object, object]) -> str:
|
||||
|
||||
|
||||
def apply_runtime_defaults() -> None:
|
||||
"""Force the small config surface required for Swagger generation."""
|
||||
"""Force the small config surface required for OpenAPI generation."""
|
||||
|
||||
os.environ.setdefault("SECRET_KEY", "spec-export")
|
||||
os.environ.setdefault("STORAGE_TYPE", "local")
|
||||
@@ -150,7 +150,7 @@ def apply_runtime_defaults() -> None:
|
||||
|
||||
|
||||
def create_spec_app() -> Flask:
|
||||
"""Build a minimal Flask app that only mounts the Swagger-producing blueprints."""
|
||||
"""Build a minimal Flask app that only mounts the OpenAPI-producing blueprints."""
|
||||
|
||||
apply_runtime_defaults()
|
||||
|
||||
@@ -182,7 +182,7 @@ def create_spec_app() -> Flask:
|
||||
|
||||
|
||||
def _registered_models(namespace: str) -> dict[str, object]:
|
||||
"""Return the Flask-RESTX models registered for a Swagger namespace."""
|
||||
"""Return the Flask-RESTX models registered for an OpenAPI namespace."""
|
||||
|
||||
if namespace == "console":
|
||||
from controllers.console import console_ns
|
||||
@@ -213,7 +213,7 @@ def _registered_models(namespace: str) -> dict[str, object]:
|
||||
models.update(api.models)
|
||||
return models
|
||||
|
||||
raise ValueError(f"unknown Swagger namespace: {namespace}")
|
||||
raise ValueError(f"unknown OpenAPI namespace: {namespace}")
|
||||
|
||||
|
||||
def _materialize_inline_model_definitions(api: RestxApi) -> None:
|
||||
@@ -289,7 +289,7 @@ def drop_null_values(value: object) -> object:
|
||||
|
||||
|
||||
def sort_openapi_arrays(value: object, *, parent_key: str | None = None) -> object:
|
||||
"""Sort order-insensitive Swagger arrays so generated Markdown is stable."""
|
||||
"""Sort order-insensitive OpenAPI arrays so generated Markdown is stable."""
|
||||
|
||||
if isinstance(value, dict):
|
||||
return {key: sort_openapi_arrays(item, parent_key=key) for key, item in value.items()}
|
||||
@@ -313,23 +313,174 @@ def sort_openapi_arrays(value: object, *, parent_key: str | None = None) -> obje
|
||||
return sorted_items
|
||||
|
||||
|
||||
def _merge_registered_definitions(payload: dict[str, object], namespace: str) -> dict[str, object]:
|
||||
"""Include registered but route-indirect models in the exported Swagger definitions."""
|
||||
def _replace_legacy_refs(value: object) -> object:
|
||||
if isinstance(value, dict):
|
||||
replaced: dict[object, object] = {}
|
||||
for key, item in value.items():
|
||||
if key == "$ref" and isinstance(item, str) and item.startswith("#/definitions/"):
|
||||
replaced[key] = item.replace("#/definitions/", "#/components/schemas/", 1)
|
||||
else:
|
||||
replaced[key] = _replace_legacy_refs(item)
|
||||
return replaced
|
||||
if isinstance(value, list):
|
||||
return [_replace_legacy_refs(item) for item in value]
|
||||
return value
|
||||
|
||||
definitions = payload.setdefault("definitions", {})
|
||||
if not isinstance(definitions, dict):
|
||||
raise RuntimeError("unexpected Swagger definitions payload")
|
||||
|
||||
HTTP_METHODS = {"delete", "get", "head", "options", "patch", "post", "put", "trace"}
|
||||
|
||||
|
||||
def _resolve_component_schema(payload: dict[str, object], schema: object) -> dict[str, object] | None:
|
||||
if not isinstance(schema, dict):
|
||||
return None
|
||||
|
||||
ref = schema.get("$ref")
|
||||
if isinstance(ref, str) and ref.startswith("#/components/schemas/"):
|
||||
name = ref.removeprefix("#/components/schemas/")
|
||||
components = payload.get("components")
|
||||
if not isinstance(components, dict):
|
||||
return None
|
||||
schemas = components.get("schemas")
|
||||
if not isinstance(schemas, dict):
|
||||
return None
|
||||
resolved = schemas.get(name)
|
||||
return resolved if isinstance(resolved, dict) else None
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
def _request_body_schema(request_body: object) -> object | None:
|
||||
if not isinstance(request_body, dict):
|
||||
return None
|
||||
content = request_body.get("content")
|
||||
if not isinstance(content, dict):
|
||||
return None
|
||||
media_type = content.get("application/json")
|
||||
if not isinstance(media_type, dict):
|
||||
return None
|
||||
return media_type.get("schema")
|
||||
|
||||
|
||||
def _query_parameters_from_schema(schema: dict[str, object]) -> list[dict[str, object]]:
|
||||
properties = schema.get("properties")
|
||||
if not isinstance(properties, dict):
|
||||
return []
|
||||
|
||||
required = schema.get("required")
|
||||
required_names = set(required) if isinstance(required, list) else set()
|
||||
parameters: list[dict[str, object]] = []
|
||||
|
||||
for name, property_schema in sorted(properties.items()):
|
||||
if not isinstance(name, str) or not isinstance(property_schema, dict):
|
||||
continue
|
||||
schema_copy = dict(property_schema)
|
||||
description = schema_copy.get("description")
|
||||
parameter: dict[str, object] = {
|
||||
"name": name,
|
||||
"in": "query",
|
||||
"required": name in required_names,
|
||||
"schema": schema_copy,
|
||||
}
|
||||
if isinstance(description, str):
|
||||
parameter["description"] = description
|
||||
parameters.append(parameter)
|
||||
|
||||
return parameters
|
||||
|
||||
|
||||
def _move_get_request_bodies_to_query_parameters(payload: dict[str, object]) -> dict[str, object]:
|
||||
"""Represent GET request bodies as query parameters in exported specs."""
|
||||
|
||||
paths = payload.get("paths")
|
||||
if not isinstance(paths, dict):
|
||||
return payload
|
||||
|
||||
for path_item in paths.values():
|
||||
if not isinstance(path_item, dict):
|
||||
continue
|
||||
operation = path_item.get("get")
|
||||
if not isinstance(operation, dict) or "requestBody" not in operation:
|
||||
continue
|
||||
|
||||
schema = _resolve_component_schema(payload, _request_body_schema(operation.get("requestBody")))
|
||||
existing_parameters = operation.get("parameters")
|
||||
parameters = list(existing_parameters) if isinstance(existing_parameters, list) else []
|
||||
existing_query_names = {
|
||||
parameter.get("name")
|
||||
for parameter in parameters
|
||||
if isinstance(parameter, dict) and parameter.get("in") == "query"
|
||||
}
|
||||
|
||||
if schema is not None:
|
||||
for parameter in _query_parameters_from_schema(schema):
|
||||
if parameter["name"] not in existing_query_names:
|
||||
parameters.append(parameter)
|
||||
|
||||
if parameters:
|
||||
operation["parameters"] = parameters
|
||||
operation.pop("requestBody", None)
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def _deduplicate_operation_ids(payload: dict[str, object]) -> dict[str, object]:
|
||||
"""Make operationId values unique while preserving already-unique IDs."""
|
||||
|
||||
paths = payload.get("paths")
|
||||
if not isinstance(paths, dict):
|
||||
return payload
|
||||
|
||||
operations_by_id: dict[str, list[tuple[str, str, dict[str, object]]]] = {}
|
||||
for path, path_item in paths.items():
|
||||
if not isinstance(path, str) or not isinstance(path_item, dict):
|
||||
continue
|
||||
for method, operation in path_item.items():
|
||||
if method not in HTTP_METHODS or not isinstance(operation, dict):
|
||||
continue
|
||||
operation_id = operation.get("operationId")
|
||||
if isinstance(operation_id, str):
|
||||
operations_by_id.setdefault(operation_id, []).append((method, path, operation))
|
||||
|
||||
for operation_id, operations in operations_by_id.items():
|
||||
if len(operations) < 2:
|
||||
continue
|
||||
for method, path, operation in operations:
|
||||
digest = hashlib.sha1(f"{method}:{path}".encode()).hexdigest()[:8]
|
||||
operation["operationId"] = f"{operation_id}_{digest}"
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def _component_schemas(payload: dict[str, object]) -> dict[str, object]:
|
||||
components = payload.setdefault("components", {})
|
||||
if not isinstance(components, dict):
|
||||
raise RuntimeError("unexpected OpenAPI components payload")
|
||||
|
||||
schemas = components.setdefault("schemas", {})
|
||||
if not isinstance(schemas, dict):
|
||||
raise RuntimeError("unexpected OpenAPI component schemas payload")
|
||||
|
||||
return schemas
|
||||
|
||||
|
||||
def _merge_registered_schemas(payload: dict[str, object], namespace: str) -> dict[str, object]:
|
||||
"""Include registered but route-indirect models in exported OpenAPI schemas."""
|
||||
|
||||
schemas = _component_schemas(payload)
|
||||
|
||||
for name, model in _registered_models(namespace).items():
|
||||
schema = getattr(model, "__schema__", None)
|
||||
if isinstance(schema, dict):
|
||||
definitions.setdefault(name, schema)
|
||||
schemas.setdefault(name, _replace_legacy_refs(schema))
|
||||
|
||||
payload.pop("definitions", None)
|
||||
payload = _replace_legacy_refs(payload) # type: ignore[assignment]
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def generate_specs(output_dir: Path) -> list[Path]:
|
||||
"""Write all Swagger specs to `output_dir` and return the written paths."""
|
||||
"""Write all OpenAPI specs to `output_dir` and return the written paths."""
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -345,7 +496,9 @@ def generate_specs(output_dir: Path) -> list[Path]:
|
||||
payload = response.get_json()
|
||||
if not isinstance(payload, dict):
|
||||
raise RuntimeError(f"unexpected response payload for {target.route}")
|
||||
payload = _merge_registered_definitions(payload, target.namespace)
|
||||
payload = _merge_registered_schemas(payload, target.namespace)
|
||||
payload = _move_get_request_bodies_to_query_parameters(payload)
|
||||
payload = _deduplicate_operation_ids(payload)
|
||||
payload = drop_null_values(payload)
|
||||
payload = sort_openapi_arrays(payload)
|
||||
|
||||
@@ -363,7 +516,7 @@ def parse_args() -> argparse.Namespace:
|
||||
"--output-dir",
|
||||
type=Path,
|
||||
default=Path("openapi"),
|
||||
help="Directory where the Swagger JSON files will be written.",
|
||||
help="Directory where the OpenAPI JSON files will be written.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ def init_app(app: DifyApp) -> None:
|
||||
docs_url=docs_url,
|
||||
redoc_url=redoc_url,
|
||||
openapi_url=openapi_url,
|
||||
openapi_version="3.0.0",
|
||||
openapi_version="3.1.0",
|
||||
title="Dify API (FastOpenAPI PoC)",
|
||||
version="1.0",
|
||||
description="FastOpenAPI proof of concept for Dify API",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Compatibility helpers for Dify's Flask-RESTX Swagger integration.
|
||||
"""Compatibility helpers for Dify's Flask-RESTX OpenAPI integration.
|
||||
|
||||
These helpers are temporary bridges for legacy Flask-RESTX field contracts
|
||||
while controllers migrate their request and response documentation to Pydantic
|
||||
models. Keep the behavior centralized so live Swagger endpoints and offline
|
||||
models. Keep the behavior centralized so live OpenAPI endpoints and offline
|
||||
spec export fail or succeed in the same way.
|
||||
"""
|
||||
|
||||
@@ -91,7 +91,7 @@ def _inline_model_signature(nested_fields: dict[object, object]) -> object:
|
||||
|
||||
|
||||
def _inline_model_name(nested_fields: dict[object, object]) -> str:
|
||||
"""Return a stable Swagger model name for an anonymous inline field map."""
|
||||
"""Return a stable OpenAPI model name for an anonymous inline field map."""
|
||||
|
||||
signature = json.dumps(_inline_model_signature(nested_fields), sort_keys=True, separators=(",", ":"))
|
||||
digest = hashlib.sha1(signature.encode("utf-8")).hexdigest()[:12]
|
||||
@@ -99,11 +99,11 @@ def _inline_model_name(nested_fields: dict[object, object]) -> str:
|
||||
|
||||
|
||||
def patch_swagger_for_inline_nested_dicts() -> None:
|
||||
"""Allow Swagger generation to handle legacy inline Flask-RESTX field dicts.
|
||||
"""Allow OpenAPI generation to handle legacy inline Flask-RESTX field dicts.
|
||||
|
||||
Some existing controllers use raw field mappings in `fields.Nested({...})`
|
||||
or directly in `@namespace.response(...)`. Runtime marshalling accepts that,
|
||||
but Flask-RESTX Swagger registration expects a named model. Convert those
|
||||
but Flask-RESTX registration expects a named model. Convert those
|
||||
anonymous mappings into temporary named models during docs generation.
|
||||
"""
|
||||
|
||||
|
||||
+4158
-5340
File diff suppressed because it is too large
Load Diff
+240
-278
@@ -3,523 +3,485 @@ User-scoped programmatic API (bearer auth)
|
||||
|
||||
## Version: 1.0
|
||||
|
||||
### Security
|
||||
**Bearer**
|
||||
|
||||
| apiKey | *API Key* |
|
||||
| ------ | --------- |
|
||||
| Description | Type: Bearer {your-api-key} |
|
||||
| In | header |
|
||||
| Name | Authorization |
|
||||
### Available authorizations
|
||||
#### Bearer (API Key Authentication)
|
||||
Type: Bearer {your-api-key}
|
||||
**Name:** Authorization
|
||||
**In:** header
|
||||
|
||||
---
|
||||
## openapi
|
||||
User-scoped operations
|
||||
|
||||
### /_health
|
||||
|
||||
#### GET
|
||||
##### Responses
|
||||
### [GET] /_health
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Health check | [HealthResponse](#healthresponse) |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
| 200 | Health check | **application/json**: [HealthResponse](#healthresponse)<br> |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
### /_version
|
||||
|
||||
#### GET
|
||||
##### Responses
|
||||
### [GET] /_version
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Server version | [ServerVersionResponse](#serverversionresponse) |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
| 200 | Server version | **application/json**: [ServerVersionResponse](#serverversionresponse)<br> |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
### /account
|
||||
|
||||
#### GET
|
||||
##### Responses
|
||||
### [GET] /account
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Account info | [AccountResponse](#accountresponse) |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
| 200 | Account info | **application/json**: [AccountResponse](#accountresponse)<br> |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
### /account/sessions
|
||||
|
||||
#### GET
|
||||
##### Parameters
|
||||
### [GET] /account/sessions
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| limit | query | | No | integer |
|
||||
| page | query | | No | integer |
|
||||
| limit | query | | No | integer, <br>**Default:** 100 |
|
||||
| page | query | | No | integer, <br>**Default:** 1 |
|
||||
|
||||
##### Responses
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Session list | [SessionListResponse](#sessionlistresponse) |
|
||||
| 422 | Validation error | [ErrorBody](#errorbody) |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
| 200 | Session list | **application/json**: [SessionListResponse](#sessionlistresponse)<br> |
|
||||
| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
### /account/sessions/self
|
||||
|
||||
#### DELETE
|
||||
##### Responses
|
||||
### [DELETE] /account/sessions/self
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Session revoked | [RevokeResponse](#revokeresponse) |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
| 200 | Session revoked | **application/json**: [RevokeResponse](#revokeresponse)<br> |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
### /account/sessions/{session_id}
|
||||
|
||||
#### DELETE
|
||||
##### Parameters
|
||||
### [DELETE] /account/sessions/{session_id}
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| session_id | path | | Yes | string |
|
||||
|
||||
##### Responses
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Session revoked | [RevokeResponse](#revokeresponse) |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
| 200 | Session revoked | **application/json**: [RevokeResponse](#revokeresponse)<br> |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
### /apps
|
||||
|
||||
#### GET
|
||||
##### Parameters
|
||||
### [GET] /apps
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| limit | query | | No | integer |
|
||||
| limit | query | | No | integer, <br>**Default:** 20 |
|
||||
| mode | query | | No | string |
|
||||
| name | query | | No | string |
|
||||
| page | query | | No | integer |
|
||||
| page | query | | No | integer, <br>**Default:** 1 |
|
||||
| tag | query | | No | string |
|
||||
| workspace_id | query | | Yes | string |
|
||||
|
||||
##### Responses
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | App list | [AppListResponse](#applistresponse) |
|
||||
| 422 | Validation error | [ErrorBody](#errorbody) |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
| 200 | App list | **application/json**: [AppListResponse](#applistresponse)<br> |
|
||||
| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
### /apps/{app_id}/check-dependencies
|
||||
|
||||
#### GET
|
||||
##### Parameters
|
||||
### [GET] /apps/{app_id}/check-dependencies
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| app_id | path | | Yes | string |
|
||||
|
||||
##### Responses
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Dependencies checked | [CheckDependenciesResult](#checkdependenciesresult) |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
| 200 | Dependencies checked | **application/json**: [CheckDependenciesResult](#checkdependenciesresult)<br> |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
### /apps/{app_id}/describe
|
||||
|
||||
#### GET
|
||||
##### Parameters
|
||||
### [GET] /apps/{app_id}/describe
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| app_id | path | | Yes | string |
|
||||
| fields | query | | No | string |
|
||||
| app_id | path | | Yes | string |
|
||||
|
||||
##### Responses
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | App description | [AppDescribeResponse](#appdescriberesponse) |
|
||||
| 422 | Validation error | [ErrorBody](#errorbody) |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
| 200 | App description | **application/json**: [AppDescribeResponse](#appdescriberesponse)<br> |
|
||||
| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
### /apps/{app_id}/export
|
||||
|
||||
#### GET
|
||||
##### Parameters
|
||||
### [GET] /apps/{app_id}/export
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| app_id | path | | Yes | string |
|
||||
| include_secret | query | Include encrypted secret values in the exported DSL | No | boolean |
|
||||
| workflow_id | query | Export a specific workflow version instead of the current draft | No | string |
|
||||
| app_id | path | | Yes | string |
|
||||
|
||||
##### Responses
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Export successful | [AppDslExportResponse](#appdslexportresponse) |
|
||||
| 422 | Validation error | [ErrorBody](#errorbody) |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
|
||||
### /apps/{app_id}/files/upload
|
||||
|
||||
#### POST
|
||||
##### Description
|
||||
| 200 | Export successful | **application/json**: [AppDslExportResponse](#appdslexportresponse)<br> |
|
||||
| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
### [POST] /apps/{app_id}/files/upload
|
||||
Upload a file to use as an input variable when running the app
|
||||
|
||||
##### Parameters
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| app_id | path | | Yes | string |
|
||||
|
||||
##### Responses
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 201 | File uploaded successfully | [FileResponse](#fileresponse) |
|
||||
| 201 | File uploaded successfully | **application/json**: [FileResponse](#fileresponse)<br> |
|
||||
| 400 | Bad request — no file or filename missing | |
|
||||
| 401 | Unauthorized — invalid or expired bearer token | |
|
||||
| 413 | File too large | |
|
||||
| 415 | Unsupported file type or blocked extension | |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
### /apps/{app_id}/form/human_input/{form_token}
|
||||
|
||||
#### GET
|
||||
##### Parameters
|
||||
### [GET] /apps/{app_id}/form/human_input/{form_token}
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| app_id | path | | Yes | string |
|
||||
| form_token | path | | Yes | string |
|
||||
|
||||
##### Responses
|
||||
#### Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Form definition |
|
||||
|
||||
#### POST
|
||||
##### Parameters
|
||||
### [POST] /apps/{app_id}/form/human_input/{form_token}
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| app_id | path | | Yes | string |
|
||||
| form_token | path | | Yes | string |
|
||||
| payload | body | | Yes | [HumanInputFormSubmitPayload](#humaninputformsubmitpayload) |
|
||||
|
||||
##### Responses
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [HumanInputFormSubmitPayload](#humaninputformsubmitpayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Form submitted | [FormSubmitResponse](#formsubmitresponse) |
|
||||
| 422 | Validation error | [ErrorBody](#errorbody) |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
| 200 | Form submitted | **application/json**: [FormSubmitResponse](#formsubmitresponse)<br> |
|
||||
| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
### /apps/{app_id}/run
|
||||
|
||||
#### POST
|
||||
##### Parameters
|
||||
### [POST] /apps/{app_id}/run
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| app_id | path | | Yes | string |
|
||||
| payload | body | | Yes | [AppRunRequest](#apprunrequest) |
|
||||
|
||||
##### Responses
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [AppRunRequest](#apprunrequest)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Run result (SSE stream) | |
|
||||
| 422 | Validation error | [ErrorBody](#errorbody) |
|
||||
| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
### /apps/{app_id}/tasks/{task_id}/events
|
||||
|
||||
#### GET
|
||||
##### Parameters
|
||||
### [GET] /apps/{app_id}/tasks/{task_id}/events
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| app_id | path | | Yes | string |
|
||||
| task_id | path | | Yes | string |
|
||||
|
||||
##### Responses
|
||||
#### Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | SSE event stream |
|
||||
|
||||
### /apps/{app_id}/tasks/{task_id}/stop
|
||||
|
||||
#### POST
|
||||
##### Parameters
|
||||
### [POST] /apps/{app_id}/tasks/{task_id}/stop
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| app_id | path | | Yes | string |
|
||||
| task_id | path | | Yes | string |
|
||||
|
||||
##### Responses
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Task stopped | [TaskStopResponse](#taskstopresponse) |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
| 200 | Task stopped | **application/json**: [TaskStopResponse](#taskstopresponse)<br> |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
### /oauth/device/approve
|
||||
### [POST] /oauth/device/approve
|
||||
#### Request Body
|
||||
|
||||
#### POST
|
||||
##### Parameters
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [DeviceMutateRequest](#devicemutaterequest)<br> |
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| payload | body | | Yes | [DeviceMutateRequest](#devicemutaterequest) |
|
||||
|
||||
##### Responses
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Approved | [DeviceMutateResponse](#devicemutateresponse) |
|
||||
| 200 | Approved | **application/json**: [DeviceMutateResponse](#devicemutateresponse)<br> |
|
||||
|
||||
### /oauth/device/code
|
||||
### [POST] /oauth/device/code
|
||||
#### Request Body
|
||||
|
||||
#### POST
|
||||
##### Parameters
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [DeviceCodeRequest](#devicecoderequest)<br> |
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| payload | body | | Yes | [DeviceCodeRequest](#devicecoderequest) |
|
||||
|
||||
##### Responses
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Device code created | [DeviceCodeResponse](#devicecoderesponse) |
|
||||
| 200 | Device code created | **application/json**: [DeviceCodeResponse](#devicecoderesponse)<br> |
|
||||
|
||||
### /oauth/device/deny
|
||||
### [POST] /oauth/device/deny
|
||||
#### Request Body
|
||||
|
||||
#### POST
|
||||
##### Parameters
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [DeviceMutateRequest](#devicemutaterequest)<br> |
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| payload | body | | Yes | [DeviceMutateRequest](#devicemutaterequest) |
|
||||
|
||||
##### Responses
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Denied | [DeviceMutateResponse](#devicemutateresponse) |
|
||||
| 200 | Denied | **application/json**: [DeviceMutateResponse](#devicemutateresponse)<br> |
|
||||
|
||||
### /oauth/device/lookup
|
||||
|
||||
#### GET
|
||||
##### Parameters
|
||||
### [GET] /oauth/device/lookup
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| user_code | query | | Yes | string |
|
||||
|
||||
##### Responses
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Device lookup result | [DeviceLookupResponse](#devicelookupresponse) |
|
||||
| 200 | Device lookup result | **application/json**: [DeviceLookupResponse](#devicelookupresponse)<br> |
|
||||
|
||||
### /oauth/device/token
|
||||
### [POST] /oauth/device/token
|
||||
#### Request Body
|
||||
|
||||
#### POST
|
||||
##### Parameters
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [DevicePollRequest](#devicepollrequest)<br> |
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| payload | body | | Yes | [DevicePollRequest](#devicepollrequest) |
|
||||
|
||||
##### Responses
|
||||
#### Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Success |
|
||||
|
||||
### /permitted-external-apps
|
||||
|
||||
#### GET
|
||||
##### Parameters
|
||||
### [GET] /permitted-external-apps
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| limit | query | | No | integer |
|
||||
| limit | query | | No | integer, <br>**Default:** 20 |
|
||||
| mode | query | | No | string |
|
||||
| name | query | | No | string |
|
||||
| page | query | | No | integer |
|
||||
| page | query | | No | integer, <br>**Default:** 1 |
|
||||
|
||||
##### Responses
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Permitted external apps list | [PermittedExternalAppsListResponse](#permittedexternalappslistresponse) |
|
||||
| 422 | Validation error | [ErrorBody](#errorbody) |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
| 200 | Permitted external apps list | **application/json**: [PermittedExternalAppsListResponse](#permittedexternalappslistresponse)<br> |
|
||||
| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
### /workspaces
|
||||
|
||||
#### GET
|
||||
##### Responses
|
||||
### [GET] /workspaces
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Workspace list | [WorkspaceListResponse](#workspacelistresponse) |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
| 200 | Workspace list | **application/json**: [WorkspaceListResponse](#workspacelistresponse)<br> |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
### /workspaces/{workspace_id}
|
||||
|
||||
#### GET
|
||||
##### Parameters
|
||||
### [GET] /workspaces/{workspace_id}
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| workspace_id | path | | Yes | string |
|
||||
|
||||
##### Responses
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Workspace detail | [WorkspaceDetailResponse](#workspacedetailresponse) |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
| 200 | Workspace detail | **application/json**: [WorkspaceDetailResponse](#workspacedetailresponse)<br> |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
### /workspaces/{workspace_id}/apps/imports
|
||||
|
||||
#### POST
|
||||
##### Parameters
|
||||
### [POST] /workspaces/{workspace_id}/apps/imports
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| workspace_id | path | | Yes | string |
|
||||
| payload | body | | Yes | [AppDslImportPayload](#appdslimportpayload) |
|
||||
|
||||
##### Responses
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [AppDslImportPayload](#appdslimportpayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Import completed | [Import](#import) |
|
||||
| 202 | Import pending confirmation | [Import](#import) |
|
||||
| 400 | Import failed | [Import](#import) |
|
||||
| 422 | Validation error | [ErrorBody](#errorbody) |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
| 200 | Import completed | **application/json**: [Import](#import)<br> |
|
||||
| 202 | Import pending confirmation | **application/json**: [Import](#import)<br> |
|
||||
| 400 | Import failed | **application/json**: [Import](#import)<br> |
|
||||
| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
### /workspaces/{workspace_id}/apps/imports/{import_id}/confirm
|
||||
|
||||
#### POST
|
||||
##### Parameters
|
||||
### [POST] /workspaces/{workspace_id}/apps/imports/{import_id}/confirm
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| import_id | path | | Yes | string |
|
||||
| workspace_id | path | | Yes | string |
|
||||
|
||||
##### Responses
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Import confirmed | [Import](#import) |
|
||||
| 400 | Import failed | [Import](#import) |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
| 200 | Import confirmed | **application/json**: [Import](#import)<br> |
|
||||
| 400 | Import failed | **application/json**: [Import](#import)<br> |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
### /workspaces/{workspace_id}/members
|
||||
### [GET] /workspaces/{workspace_id}/members
|
||||
#### Parameters
|
||||
|
||||
#### GET
|
||||
##### Parameters
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| limit | query | | No | integer, <br>**Default:** 20 |
|
||||
| page | query | | No | integer, <br>**Default:** 1 |
|
||||
| workspace_id | path | | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Member list | **application/json**: [MemberListResponse](#memberlistresponse)<br> |
|
||||
| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
### [POST] /workspaces/{workspace_id}/members
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| workspace_id | path | | Yes | string |
|
||||
| limit | query | | No | integer |
|
||||
| page | query | | No | integer |
|
||||
|
||||
##### Responses
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [MemberInvitePayload](#memberinvitepayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Member list | [MemberListResponse](#memberlistresponse) |
|
||||
| 422 | Validation error | [ErrorBody](#errorbody) |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
| 201 | Member invited | **application/json**: [MemberInviteResponse](#memberinviteresponse)<br> |
|
||||
| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
#### POST
|
||||
##### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| workspace_id | path | | Yes | string |
|
||||
| payload | body | | Yes | [MemberInvitePayload](#memberinvitepayload) |
|
||||
|
||||
##### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 201 | Member invited | [MemberInviteResponse](#memberinviteresponse) |
|
||||
| 422 | Validation error | [ErrorBody](#errorbody) |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
|
||||
### /workspaces/{workspace_id}/members/{member_id}
|
||||
|
||||
#### DELETE
|
||||
##### Parameters
|
||||
### [DELETE] /workspaces/{workspace_id}/members/{member_id}
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| member_id | path | | Yes | string |
|
||||
| workspace_id | path | | Yes | string |
|
||||
|
||||
##### Responses
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Member removed | [MemberActionResponse](#memberactionresponse) |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
| 200 | Member removed | **application/json**: [MemberActionResponse](#memberactionresponse)<br> |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
### /workspaces/{workspace_id}/members/{member_id}/role
|
||||
|
||||
#### PUT
|
||||
##### Parameters
|
||||
### [PUT] /workspaces/{workspace_id}/members/{member_id}/role
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| member_id | path | | Yes | string |
|
||||
| workspace_id | path | | Yes | string |
|
||||
| payload | body | | Yes | [MemberRoleUpdatePayload](#memberroleupdatepayload) |
|
||||
|
||||
##### Responses
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [MemberRoleUpdatePayload](#memberroleupdatepayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Role updated | [MemberActionResponse](#memberactionresponse) |
|
||||
| 422 | Validation error | [ErrorBody](#errorbody) |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
| 200 | Role updated | **application/json**: [MemberActionResponse](#memberactionresponse)<br> |
|
||||
| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
### /workspaces/{workspace_id}/switch
|
||||
|
||||
#### POST
|
||||
##### Parameters
|
||||
### [POST] /workspaces/{workspace_id}/switch
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| workspace_id | path | | Yes | string |
|
||||
|
||||
##### Responses
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Workspace detail | [WorkspaceDetailResponse](#workspacedetailresponse) |
|
||||
| default | Error | [ErrorBody](#errorbody) |
|
||||
| 200 | Workspace detail | **application/json**: [WorkspaceDetailResponse](#workspacedetailresponse)<br> |
|
||||
| default | Error | **application/json**: [ErrorBody](#errorbody)<br> |
|
||||
|
||||
---
|
||||
### Models
|
||||
### Schemas
|
||||
|
||||
#### AccountPayload
|
||||
|
||||
@@ -538,7 +500,7 @@ Upload a file to use as an input variable when running the app
|
||||
| subject_email | string | | No |
|
||||
| subject_issuer | string | | No |
|
||||
| subject_type | string | | Yes |
|
||||
| workspaces | [ [WorkspacePayload](#workspacepayload) ] | | No |
|
||||
| workspaces | [ [WorkspacePayload](#workspacepayload) ], <br>**Default:** | | No |
|
||||
|
||||
#### AppDescribeInfo
|
||||
|
||||
@@ -551,7 +513,7 @@ Upload a file to use as an input variable when running the app
|
||||
| mode | string | | Yes |
|
||||
| name | string | | Yes |
|
||||
| service_api_enabled | boolean | | Yes |
|
||||
| tags | [ [TagItem](#tagitem) ] | | No |
|
||||
| tags | [ [TagItem](#tagitem) ], <br>**Default:** | | No |
|
||||
| updated_at | string | | No |
|
||||
|
||||
#### AppDescribeQuery
|
||||
@@ -600,7 +562,7 @@ Request body for POST /workspaces/<workspace_id>/apps/imports.
|
||||
| icon | string | | No |
|
||||
| icon_background | string | | No |
|
||||
| icon_type | string | | No |
|
||||
| mode | string | Import mode: yaml-content or yaml-url<br>*Enum:* `"yaml-content"`, `"yaml-url"` | Yes |
|
||||
| mode | string, <br>**Available values:** "yaml-content", "yaml-url" | Import mode: yaml-content or yaml-url<br>*Enum:* `"yaml-content"`, `"yaml-url"` | Yes |
|
||||
| name | string | Override the app name from the DSL | No |
|
||||
| yaml_content | string | Inline YAML DSL string (required when mode is yaml-content) | No |
|
||||
| yaml_url | string | Remote URL to fetch YAML from (required when mode is yaml-url) | No |
|
||||
@@ -614,7 +576,7 @@ Request body for POST /workspaces/<workspace_id>/apps/imports.
|
||||
| id | string | | Yes |
|
||||
| mode | string | | Yes |
|
||||
| name | string | | Yes |
|
||||
| tags | [ [TagItem](#tagitem) ] | | No |
|
||||
| tags | [ [TagItem](#tagitem) ], <br>**Default:** | | No |
|
||||
|
||||
#### AppListQuery
|
||||
|
||||
@@ -622,10 +584,10 @@ mode is a closed enum.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| limit | integer | | No |
|
||||
| limit | integer, <br>**Default:** 20 | | No |
|
||||
| mode | [AppMode](#appmode) | | No |
|
||||
| name | string | | No |
|
||||
| page | integer | | No |
|
||||
| page | integer, <br>**Default:** 1 | | No |
|
||||
| tag | string | | No |
|
||||
| workspace_id | string | | Yes |
|
||||
|
||||
@@ -648,7 +610,7 @@ mode is a closed enum.
|
||||
| id | string | | Yes |
|
||||
| mode | [AppMode](#appmode) | | Yes |
|
||||
| name | string | | Yes |
|
||||
| tags | [ [TagItem](#tagitem) ] | | No |
|
||||
| tags | [ [TagItem](#tagitem) ], <br>**Default:** | | No |
|
||||
| updated_at | string | | No |
|
||||
| workspace_id | string | | No |
|
||||
| workspace_name | string | | No |
|
||||
@@ -663,7 +625,7 @@ mode is a closed enum.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| auto_generate_name | boolean | | No |
|
||||
| auto_generate_name | boolean, <br>**Default:** true | | No |
|
||||
| conversation_id | string | | No |
|
||||
| files | [ object ] | | No |
|
||||
| inputs | object | | Yes |
|
||||
@@ -745,7 +707,7 @@ future server adds a code. Formatter tests pin emitted values to the enum.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| loc | [ ] | | No |
|
||||
| loc | [ ], <br>**Default:** | | No |
|
||||
| msg | string | | Yes |
|
||||
| type | string | | Yes |
|
||||
|
||||
@@ -808,7 +770,7 @@ Liveness payload for `GET /openapi/v1/_health` — no auth required.
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| app_id | string | | No |
|
||||
| app_mode | string | | No |
|
||||
| current_dsl_version | string | | No |
|
||||
| current_dsl_version | string, <br>**Default:** 0.6.0 | | No |
|
||||
| error | string | | No |
|
||||
| id | string | | Yes |
|
||||
| imported_dsl_version | string | | No |
|
||||
@@ -837,14 +799,14 @@ Liveness payload for `GET /openapi/v1/_health` — no auth required.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| result | string | | No |
|
||||
| result | string, <br>**Default:** success | | No |
|
||||
|
||||
#### MemberInvitePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| email | string | | Yes |
|
||||
| role | string | *Enum:* `"admin"`, `"normal"` | Yes |
|
||||
| role | string, <br>**Available values:** "admin", "normal" | *Enum:* `"admin"`, `"normal"` | Yes |
|
||||
|
||||
#### MemberInviteResponse
|
||||
|
||||
@@ -853,7 +815,7 @@ Liveness payload for `GET /openapi/v1/_health` — no auth required.
|
||||
| email | string | | Yes |
|
||||
| invite_url | string | | Yes |
|
||||
| member_id | string | | Yes |
|
||||
| result | string | | No |
|
||||
| result | string, <br>**Default:** success | | No |
|
||||
| role | string | | Yes |
|
||||
| tenant_id | string | | Yes |
|
||||
|
||||
@@ -863,8 +825,8 @@ Strict (extra='forbid').
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| limit | integer | | No |
|
||||
| page | integer | | No |
|
||||
| limit | integer, <br>**Default:** 20 | | No |
|
||||
| page | integer, <br>**Default:** 1 | | No |
|
||||
|
||||
#### MemberListResponse
|
||||
|
||||
@@ -891,13 +853,13 @@ Strict (extra='forbid').
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| role | string | *Enum:* `"admin"`, `"normal"` | Yes |
|
||||
| role | string, <br>**Available values:** "admin", "normal" | *Enum:* `"admin"`, `"normal"` | Yes |
|
||||
|
||||
#### MessageMetadata
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| retriever_resources | [ object ] | | No |
|
||||
| retriever_resources | [ object ], <br>**Default:** | | No |
|
||||
| usage | [UsageInfo](#usageinfo) | | No |
|
||||
|
||||
#### OpenApiErrorCode
|
||||
@@ -919,10 +881,10 @@ Strict (extra='forbid').
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| limit | integer | | No |
|
||||
| limit | integer, <br>**Default:** 20 | | No |
|
||||
| mode | [AppMode](#appmode) | | No |
|
||||
| name | string | | No |
|
||||
| page | integer | | No |
|
||||
| page | integer, <br>**Default:** 1 | | No |
|
||||
|
||||
#### PermittedExternalAppsListResponse
|
||||
|
||||
@@ -954,7 +916,7 @@ Meta endpoint payload for `GET /openapi/v1/_version` — no auth required.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| edition | string | *Enum:* `"CLOUD"`, `"SELF_HOSTED"` | Yes |
|
||||
| edition | string, <br>**Available values:** "CLOUD", "SELF_HOSTED" | *Enum:* `"CLOUD"`, `"SELF_HOSTED"` | Yes |
|
||||
| version | string | | Yes |
|
||||
|
||||
#### SessionListQuery
|
||||
@@ -963,8 +925,8 @@ Pagination for GET /account/sessions. Strict (extra='forbid').
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| limit | integer | | No |
|
||||
| page | integer | | No |
|
||||
| limit | integer, <br>**Default:** 100 | | No |
|
||||
| page | integer, <br>**Default:** 1 | | No |
|
||||
|
||||
#### SessionListResponse
|
||||
|
||||
+644
-962
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -60,6 +60,7 @@ exclude = ["providers/vdb/__pycache__", "providers/trace/__pycache__"]
|
||||
|
||||
[tool.uv.sources]
|
||||
dify-agent = { path = "../dify-agent", editable = true }
|
||||
flask-restx = { git = "https://github.com/asukaminato0721/flask-restx", rev = "27758e26f8f740d7525d5039c51a9e524b6e2b68" }
|
||||
dify-vdb-alibabacloud-mysql = { workspace = true }
|
||||
dify-vdb-analyticdb = { workspace = true }
|
||||
dify-vdb-baidu = { workspace = true }
|
||||
|
||||
@@ -22,7 +22,7 @@ def _load_generate_swagger_markdown_docs_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"
|
||||
openapi_dir = tmp_path / "openapi"
|
||||
markdown_dir = tmp_path / "markdown"
|
||||
stale_combined_doc = markdown_dir / "api-reference.md"
|
||||
markdown_dir.mkdir()
|
||||
@@ -50,23 +50,23 @@ def test_generate_markdown_docs_keeps_split_docs_and_merges_fastopenapi_into_con
|
||||
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)
|
||||
written_paths = module.generate_markdown_docs(openapi_dir, markdown_dir)
|
||||
|
||||
assert [path.name for path in written_paths] == [
|
||||
"console-swagger.md",
|
||||
"web-swagger.md",
|
||||
"service-swagger.md",
|
||||
"openapi-swagger.md",
|
||||
"console-openapi.md",
|
||||
"web-openapi.md",
|
||||
"service-openapi.md",
|
||||
"openapi-openapi.md",
|
||||
]
|
||||
assert not stale_combined_doc.exists()
|
||||
assert not list(swagger_dir.glob("*.json"))
|
||||
assert not list(openapi_dir.glob("*.json"))
|
||||
|
||||
console_markdown = (markdown_dir / "console-swagger.md").read_text(encoding="utf-8")
|
||||
assert "## FastOpenAPI Preview (OpenAPI 3.0)" in console_markdown
|
||||
console_markdown = (markdown_dir / "console-openapi.md").read_text(encoding="utf-8")
|
||||
assert "## FastOpenAPI Preview (OpenAPI 3.1)" 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")
|
||||
assert "FastOpenAPI Preview" not in (markdown_dir / "web-openapi.md").read_text(encoding="utf-8")
|
||||
assert "FastOpenAPI Preview" not in (markdown_dir / "service-openapi.md").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_generate_markdown_docs_only_removes_generated_specs_from_separate_swagger_dir(tmp_path, monkeypatch):
|
||||
@@ -107,39 +107,41 @@ def test_generate_markdown_docs_only_removes_generated_specs_from_separate_swagg
|
||||
|
||||
def test_patch_union_schema_markdown_fills_converter_blank_schema_types(tmp_path):
|
||||
module = _load_generate_swagger_markdown_docs_module()
|
||||
spec_path = tmp_path / "console-swagger.json"
|
||||
spec_path = tmp_path / "console-openapi.json"
|
||||
spec_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"definitions": {
|
||||
"FormInputConfig": {
|
||||
"oneOf": [
|
||||
{"$ref": "#/definitions/ParagraphInputConfig"},
|
||||
{"$ref": "#/definitions/SelectInputConfig"},
|
||||
{"$ref": "#/definitions/FileInputConfig"},
|
||||
],
|
||||
},
|
||||
"ParagraphInputConfig": {
|
||||
"properties": {
|
||||
"default": {
|
||||
"anyOf": [
|
||||
{"$ref": "#/definitions/StringSource"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"components": {
|
||||
"schemas": {
|
||||
"FormInputConfig": {
|
||||
"oneOf": [
|
||||
{"$ref": "#/components/schemas/ParagraphInputConfig"},
|
||||
{"$ref": "#/components/schemas/SelectInputConfig"},
|
||||
{"$ref": "#/components/schemas/FileInputConfig"},
|
||||
],
|
||||
},
|
||||
"ParagraphInputConfig": {
|
||||
"properties": {
|
||||
"default": {
|
||||
"anyOf": [
|
||||
{"$ref": "#/components/schemas/StringSource"},
|
||||
{"type": "null"},
|
||||
],
|
||||
},
|
||||
"output_variable_name": {"type": "string"},
|
||||
},
|
||||
"output_variable_name": {"type": "string"},
|
||||
},
|
||||
},
|
||||
"SelectInputConfig": {
|
||||
"properties": {
|
||||
"option_source": {"$ref": "#/definitions/StringListSource"},
|
||||
"SelectInputConfig": {
|
||||
"properties": {
|
||||
"option_source": {"$ref": "#/components/schemas/StringListSource"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"FileInputConfig": {
|
||||
"properties": {
|
||||
"allowed_file_types": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/definitions/FileType"},
|
||||
"FileInputConfig": {
|
||||
"properties": {
|
||||
"allowed_file_types": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/FileType"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -188,24 +190,26 @@ def test_patch_union_schema_markdown_fills_converter_blank_schema_types(tmp_path
|
||||
assert "| allowed_file_types | [ [FileType](#filetype) ] | | No |" in patched
|
||||
|
||||
|
||||
def test_patch_union_schema_markdown_fills_regular_definition_union_property(tmp_path):
|
||||
def test_patch_union_schema_markdown_fills_regular_schema_union_property(tmp_path):
|
||||
module = _load_generate_swagger_markdown_docs_module()
|
||||
spec_path = tmp_path / "service-swagger.json"
|
||||
spec_path = tmp_path / "service-openapi.json"
|
||||
spec_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"definitions": {
|
||||
"DocumentMetadataResponse": {
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
"value": {
|
||||
"anyOf": [
|
||||
{"type": "string"},
|
||||
{"type": "integer"},
|
||||
{"type": "number"},
|
||||
{"type": "boolean"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"components": {
|
||||
"schemas": {
|
||||
"DocumentMetadataResponse": {
|
||||
"properties": {
|
||||
"id": {"type": "string"},
|
||||
"value": {
|
||||
"anyOf": [
|
||||
{"type": "string"},
|
||||
{"type": "integer"},
|
||||
{"type": "number"},
|
||||
{"type": "boolean"},
|
||||
{"type": "null"},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -227,9 +231,9 @@ def test_patch_union_schema_markdown_fills_regular_definition_union_property(tmp
|
||||
assert "| value | string<br>integer<br>number<br>boolean | | No |" in patched
|
||||
|
||||
|
||||
def test_patch_union_schema_markdown_ignores_specs_without_definitions(tmp_path):
|
||||
def test_patch_union_schema_markdown_ignores_specs_without_schemas(tmp_path):
|
||||
module = _load_generate_swagger_markdown_docs_module()
|
||||
spec_path = tmp_path / "console-swagger.json"
|
||||
spec_path = tmp_path / "console-openapi.json"
|
||||
spec_path.write_text("{}", encoding="utf-8")
|
||||
|
||||
assert module._patch_union_schema_markdown("unchanged", spec_path) == "unchanged"
|
||||
@@ -237,27 +241,29 @@ def test_patch_union_schema_markdown_ignores_specs_without_definitions(tmp_path)
|
||||
|
||||
def test_patch_union_schema_markdown_ignores_unrenderable_shapes(tmp_path):
|
||||
module = _load_generate_swagger_markdown_docs_module()
|
||||
spec_path = tmp_path / "console-swagger.json"
|
||||
spec_path = tmp_path / "console-openapi.json"
|
||||
spec_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"definitions": {
|
||||
"NotAMapping": [],
|
||||
"BrokenUnion": {
|
||||
"oneOf": [
|
||||
{},
|
||||
{"$ref": "#/definitions/Missing"},
|
||||
{"$ref": "#/definitions/NoPropertyMapping"},
|
||||
],
|
||||
"components": {
|
||||
"schemas": {
|
||||
"NotAMapping": [],
|
||||
"BrokenUnion": {
|
||||
"oneOf": [
|
||||
{},
|
||||
{"$ref": "#/components/schemas/Missing"},
|
||||
{"$ref": "#/components/schemas/NoPropertyMapping"},
|
||||
],
|
||||
},
|
||||
"NoPropertyMapping": {"properties": []},
|
||||
},
|
||||
"NoPropertyMapping": {"properties": []},
|
||||
}
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
assert module._definition_ref_name(None) is None
|
||||
assert module._schema_ref_name(None) is None
|
||||
assert module._schema_markdown_type(None) == ""
|
||||
assert module._schema_markdown_type({"anyOf": [{"type": "null"}]}) == ""
|
||||
assert module._replace_schema_table_type("unchanged", "Definition", "field", "") == "unchanged"
|
||||
@@ -280,24 +286,26 @@ def test_patch_union_schema_markdown_ignores_unrenderable_shapes(tmp_path):
|
||||
|
||||
def test_convert_spec_to_markdown_patches_generated_union_tables(tmp_path, monkeypatch):
|
||||
module = _load_generate_swagger_markdown_docs_module()
|
||||
spec_path = tmp_path / "console-swagger.json"
|
||||
output_path = tmp_path / "console-swagger.md"
|
||||
spec_path = tmp_path / "console-openapi.json"
|
||||
output_path = tmp_path / "console-openapi.md"
|
||||
spec_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"definitions": {
|
||||
"FormInputConfig": {
|
||||
"oneOf": [
|
||||
{"$ref": "#/definitions/ParagraphInputConfig"},
|
||||
],
|
||||
},
|
||||
"ParagraphInputConfig": {
|
||||
"properties": {
|
||||
"default": {
|
||||
"anyOf": [
|
||||
{"$ref": "#/definitions/StringSource"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"components": {
|
||||
"schemas": {
|
||||
"FormInputConfig": {
|
||||
"oneOf": [
|
||||
{"$ref": "#/components/schemas/ParagraphInputConfig"},
|
||||
],
|
||||
},
|
||||
"ParagraphInputConfig": {
|
||||
"properties": {
|
||||
"default": {
|
||||
"anyOf": [
|
||||
{"$ref": "#/components/schemas/StringSource"},
|
||||
{"type": "null"},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Unit tests for the standalone Swagger export helper."""
|
||||
"""Unit tests for the standalone OpenAPI export helper."""
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
@@ -30,42 +30,82 @@ def _load_generate_swagger_specs_module():
|
||||
return module
|
||||
|
||||
|
||||
def test_generate_specs_writes_console_web_and_service_swagger_files(tmp_path):
|
||||
def _operation_ids(payload):
|
||||
methods = {"delete", "get", "head", "options", "patch", "post", "put", "trace"}
|
||||
for path_item in payload["paths"].values():
|
||||
for method, operation in path_item.items():
|
||||
if method in methods and isinstance(operation, dict) and "operationId" in operation:
|
||||
yield operation["operationId"]
|
||||
|
||||
|
||||
def _get_operations(payload):
|
||||
for path_item in payload["paths"].values():
|
||||
operation = path_item.get("get")
|
||||
if isinstance(operation, dict):
|
||||
yield operation
|
||||
|
||||
|
||||
def test_generate_specs_writes_console_web_and_service_openapi_files(tmp_path):
|
||||
module = _load_generate_swagger_specs_module()
|
||||
|
||||
written_paths = module.generate_specs(tmp_path)
|
||||
|
||||
assert [path.name for path in written_paths] == [
|
||||
"console-swagger.json",
|
||||
"web-swagger.json",
|
||||
"service-swagger.json",
|
||||
"openapi-swagger.json",
|
||||
"console-openapi.json",
|
||||
"web-openapi.json",
|
||||
"service-openapi.json",
|
||||
"openapi-openapi.json",
|
||||
]
|
||||
|
||||
for path in written_paths:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
assert payload["swagger"] == "2.0"
|
||||
assert payload["openapi"].startswith("3.")
|
||||
assert "paths" in payload
|
||||
|
||||
|
||||
def test_generate_specs_writes_swagger_with_resolvable_references_and_no_nulls(tmp_path):
|
||||
def test_generate_specs_writes_openapi_with_resolvable_references_and_no_nulls(tmp_path):
|
||||
module = _load_generate_swagger_specs_module()
|
||||
|
||||
written_paths = module.generate_specs(tmp_path)
|
||||
|
||||
for path in written_paths:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
definitions = payload["definitions"]
|
||||
schemas = payload["components"]["schemas"]
|
||||
refs = {
|
||||
item["$ref"].removeprefix("#/definitions/")
|
||||
item["$ref"].removeprefix("#/components/schemas/")
|
||||
for item in _walk_values(payload)
|
||||
if isinstance(item, dict) and isinstance(item.get("$ref"), str)
|
||||
if isinstance(item, dict)
|
||||
and isinstance(item.get("$ref"), str)
|
||||
and item["$ref"].startswith("#/components/schemas/")
|
||||
}
|
||||
|
||||
assert refs <= set(definitions)
|
||||
assert refs <= set(schemas)
|
||||
assert all(value is not None for value in _walk_values(payload))
|
||||
|
||||
|
||||
def test_generate_specs_writes_unique_operation_ids(tmp_path):
|
||||
module = _load_generate_swagger_specs_module()
|
||||
|
||||
written_paths = module.generate_specs(tmp_path)
|
||||
|
||||
for path in written_paths:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
operation_ids = list(_operation_ids(payload))
|
||||
|
||||
assert len(operation_ids) == len(set(operation_ids))
|
||||
|
||||
|
||||
def test_generate_specs_moves_get_request_bodies_to_query_parameters(tmp_path):
|
||||
module = _load_generate_swagger_specs_module()
|
||||
|
||||
written_paths = module.generate_specs(tmp_path)
|
||||
|
||||
for path in written_paths:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
assert all("requestBody" not in operation for operation in _get_operations(payload))
|
||||
|
||||
|
||||
def test_generate_specs_is_idempotent(tmp_path):
|
||||
module = _load_generate_swagger_specs_module()
|
||||
|
||||
|
||||
@@ -78,9 +78,9 @@ def mock_console_ns():
|
||||
|
||||
|
||||
def test_default_ref_template_value():
|
||||
from controllers.common.schema import DEFAULT_REF_TEMPLATE_SWAGGER_2_0
|
||||
from controllers.common.schema import DEFAULT_REF_TEMPLATE_OPENAPI_3_0
|
||||
|
||||
assert DEFAULT_REF_TEMPLATE_SWAGGER_2_0 == "#/definitions/{model}"
|
||||
assert DEFAULT_REF_TEMPLATE_OPENAPI_3_0 == "#/components/schemas/{model}"
|
||||
|
||||
|
||||
def test_register_schema_model_calls_namespace_schema_model():
|
||||
@@ -100,7 +100,7 @@ def test_register_schema_model_calls_namespace_schema_model():
|
||||
|
||||
|
||||
def test_register_schema_model_passes_schema_from_pydantic():
|
||||
from controllers.common.schema import DEFAULT_REF_TEMPLATE_SWAGGER_2_0, register_schema_model
|
||||
from controllers.common.schema import DEFAULT_REF_TEMPLATE_OPENAPI_3_0, register_schema_model
|
||||
|
||||
namespace = MagicMock(spec=Namespace)
|
||||
|
||||
@@ -108,24 +108,24 @@ def test_register_schema_model_passes_schema_from_pydantic():
|
||||
|
||||
schema = namespace.schema_model.call_args.args[1]
|
||||
|
||||
expected_schema = UserModel.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0)
|
||||
expected_schema = UserModel.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0)
|
||||
|
||||
assert schema == expected_schema
|
||||
|
||||
|
||||
def test_register_schema_model_promotes_nested_pydantic_definitions():
|
||||
from controllers.common.schema import DEFAULT_REF_TEMPLATE_SWAGGER_2_0, register_schema_model
|
||||
from controllers.common.schema import DEFAULT_REF_TEMPLATE_OPENAPI_3_0, register_schema_model
|
||||
|
||||
namespace = MagicMock(spec=Namespace)
|
||||
|
||||
register_schema_model(namespace, ParentModel)
|
||||
|
||||
called_schemas = {call.args[0]: call.args[1] for call in namespace.schema_model.call_args_list}
|
||||
parent_schema = ParentModel.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0)
|
||||
parent_schema = ParentModel.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0)
|
||||
|
||||
assert set(called_schemas) == {"ParentModel", "ChildModel"}
|
||||
assert "$defs" not in called_schemas["ParentModel"]
|
||||
assert called_schemas["ParentModel"]["properties"]["child"]["$ref"] == "#/definitions/ChildModel"
|
||||
assert called_schemas["ParentModel"]["properties"]["child"]["$ref"] == "#/components/schemas/ChildModel"
|
||||
assert called_schemas["ChildModel"] == parent_schema["$defs"]["ChildModel"]
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ def test_register_response_schema_model_uses_serialized_field_names():
|
||||
assert "internal_name" not in schema["properties"]
|
||||
|
||||
|
||||
def test_register_schema_model_flattens_simple_nullable_any_of_for_swagger_2():
|
||||
def test_register_schema_model_preserves_openapi_nullable_unions():
|
||||
from controllers.common.schema import register_schema_model
|
||||
|
||||
namespace = MagicMock(spec=Namespace)
|
||||
@@ -189,14 +189,9 @@ def test_register_schema_model_flattens_simple_nullable_any_of_for_swagger_2():
|
||||
called_schemas = {call.args[0]: call.args[1] for call in namespace.schema_model.call_args_list}
|
||||
properties = called_schemas["NullableSchemaModel"]["properties"]
|
||||
|
||||
assert properties["name"]["type"] == "string"
|
||||
assert properties["name"]["x-nullable"] is True
|
||||
assert "anyOf" not in properties["name"]
|
||||
assert properties["tags"]["type"] == "array"
|
||||
assert properties["tags"]["items"] == {"type": "string"}
|
||||
assert properties["tags"]["x-nullable"] is True
|
||||
assert properties["owner"]["$ref"] == "#/definitions/UserModel"
|
||||
assert properties["owner"]["x-nullable"] is True
|
||||
assert properties["name"]["anyOf"] == [{"type": "string"}, {"type": "null"}]
|
||||
assert properties["tags"]["anyOf"] == [{"items": {"type": "string"}, "type": "array"}, {"type": "null"}]
|
||||
assert properties["owner"]["anyOf"] == [{"$ref": "#/components/schemas/UserModel"}, {"type": "null"}]
|
||||
assert "anyOf" in properties["ambiguous"]
|
||||
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
"""Swagger JSON rendering tests for Flask-RESTX API blueprints."""
|
||||
"""OpenAPI JSON rendering tests for Flask-RESTX API blueprints."""
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
|
||||
def _definition_refs(value: object) -> set[str]:
|
||||
def _schema_refs(value: object) -> set[str]:
|
||||
refs: set[str] = set()
|
||||
if isinstance(value, dict):
|
||||
ref = value.get("$ref")
|
||||
if isinstance(ref, str) and ref.startswith("#/definitions/"):
|
||||
refs.add(ref.removeprefix("#/definitions/"))
|
||||
if isinstance(ref, str) and ref.startswith("#/components/schemas/"):
|
||||
refs.add(ref.removeprefix("#/components/schemas/"))
|
||||
for item in value.values():
|
||||
refs.update(_definition_refs(item))
|
||||
refs.update(_schema_refs(item))
|
||||
elif isinstance(value, list):
|
||||
for item in value:
|
||||
refs.update(_definition_refs(item))
|
||||
refs.update(_schema_refs(item))
|
||||
return refs
|
||||
|
||||
|
||||
@@ -31,6 +31,18 @@ def _parameters_by_name(operation: dict[str, object]) -> dict[str, dict[str, obj
|
||||
return result
|
||||
|
||||
|
||||
def _multipart_form_schema(operation: dict[str, object]) -> dict[str, object]:
|
||||
request_body = operation.get("requestBody")
|
||||
assert isinstance(request_body, dict)
|
||||
content = request_body.get("content")
|
||||
assert isinstance(content, dict)
|
||||
multipart = content.get("multipart/form-data")
|
||||
assert isinstance(multipart, dict)
|
||||
schema = multipart.get("schema")
|
||||
assert isinstance(schema, dict)
|
||||
return schema
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("first_kwargs", "second_kwargs"),
|
||||
[
|
||||
@@ -53,7 +65,7 @@ def test_inline_model_name_includes_list_constraints(
|
||||
assert _inline_model_name(first_inline_model) != _inline_model_name(second_inline_model)
|
||||
|
||||
|
||||
def test_swagger_json_endpoints_render(monkeypatch: pytest.MonkeyPatch):
|
||||
def test_openapi_json_endpoints_render(monkeypatch: pytest.MonkeyPatch):
|
||||
from configs import dify_config
|
||||
from controllers.console import bp as console_bp
|
||||
from controllers.service_api import bp as service_api_bp
|
||||
@@ -70,17 +82,17 @@ def test_swagger_json_endpoints_render(monkeypatch: pytest.MonkeyPatch):
|
||||
|
||||
client = app.test_client()
|
||||
|
||||
for route in ("/console/api/swagger.json", "/api/swagger.json", "/v1/swagger.json"):
|
||||
for route in ("/console/api/openapi.json", "/api/openapi.json", "/v1/openapi.json"):
|
||||
response = client.get(route)
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.get_json()
|
||||
assert payload["swagger"] == "2.0"
|
||||
assert payload["openapi"].startswith("3.")
|
||||
assert "paths" in payload
|
||||
assert "definitions" in payload
|
||||
assert isinstance(payload["definitions"], dict)
|
||||
missing_refs = _definition_refs(payload) - set(payload["definitions"])
|
||||
assert not sorted(ref for ref in missing_refs if ref.startswith("_AnonymousInlineModel"))
|
||||
assert "schemas" in payload["components"]
|
||||
assert isinstance(payload["components"]["schemas"], dict)
|
||||
missing_refs = _schema_refs(payload) - set(payload["components"]["schemas"])
|
||||
assert not missing_refs
|
||||
|
||||
assert app.config["RESTX_INCLUDE_ALL_MODELS"] is True
|
||||
|
||||
@@ -96,17 +108,17 @@ def test_service_document_file_routes_document_multipart_form_data(monkeypatch:
|
||||
app.config["RESTX_INCLUDE_ALL_MODELS"] = True
|
||||
app.register_blueprint(service_api_bp)
|
||||
|
||||
payload = app.test_client().get("/v1/swagger.json").get_json()
|
||||
payload = app.test_client().get("/v1/openapi.json").get_json()
|
||||
paths = payload["paths"]
|
||||
|
||||
create_operation = paths["/datasets/{dataset_id}/document/create-by-file"]["post"]
|
||||
create_params = _parameters_by_name(create_operation)
|
||||
assert create_operation["consumes"] == ["multipart/form-data"]
|
||||
assert create_params["file"]["in"] == "formData"
|
||||
assert create_params["file"]["type"] == "file"
|
||||
assert create_params["file"]["required"] is True
|
||||
assert create_params["data"]["in"] == "formData"
|
||||
assert create_params["data"]["type"] == "string"
|
||||
create_schema = _multipart_form_schema(create_operation)
|
||||
create_properties = create_schema["properties"]
|
||||
assert isinstance(create_properties, dict)
|
||||
assert create_properties["file"] == {"type": "string", "format": "binary"}
|
||||
assert create_properties["data"] == {"type": "string"}
|
||||
assert create_schema["required"] == ["file"]
|
||||
assert create_operation["requestBody"]["required"] is True
|
||||
|
||||
for path in (
|
||||
"/datasets/{dataset_id}/documents/{document_id}",
|
||||
@@ -114,13 +126,13 @@ def test_service_document_file_routes_document_multipart_form_data(monkeypatch:
|
||||
"/datasets/{dataset_id}/documents/{document_id}/update_by_file",
|
||||
):
|
||||
update_operation = paths[path]["patch" if path.endswith("{document_id}") else "post"]
|
||||
update_params = _parameters_by_name(update_operation)
|
||||
assert update_operation["consumes"] == ["multipart/form-data"]
|
||||
assert update_params["file"]["in"] == "formData"
|
||||
assert update_params["file"]["type"] == "file"
|
||||
assert update_params["file"]["required"] is False
|
||||
assert update_params["data"]["in"] == "formData"
|
||||
assert update_params["data"]["type"] == "string"
|
||||
update_schema = _multipart_form_schema(update_operation)
|
||||
update_properties = update_schema["properties"]
|
||||
assert isinstance(update_properties, dict)
|
||||
assert update_properties["file"] == {"type": "string", "format": "binary"}
|
||||
assert update_properties["data"] == {"type": "string"}
|
||||
assert "required" not in update_schema
|
||||
assert update_operation["requestBody"]["required"] is False
|
||||
|
||||
|
||||
def test_service_document_list_documents_query_params_render(monkeypatch: pytest.MonkeyPatch):
|
||||
@@ -134,7 +146,7 @@ def test_service_document_list_documents_query_params_render(monkeypatch: pytest
|
||||
app.config["RESTX_INCLUDE_ALL_MODELS"] = True
|
||||
app.register_blueprint(service_api_bp)
|
||||
|
||||
payload = app.test_client().get("/v1/swagger.json").get_json()
|
||||
payload = app.test_client().get("/v1/openapi.json").get_json()
|
||||
operation = payload["paths"]["/datasets/{dataset_id}/documents"]["get"]
|
||||
params = _parameters_by_name(operation)
|
||||
|
||||
@@ -153,7 +165,7 @@ def test_console_account_avatar_query_param_renders_as_query(monkeypatch: pytest
|
||||
app.config["RESTX_INCLUDE_ALL_MODELS"] = True
|
||||
app.register_blueprint(console_bp)
|
||||
|
||||
payload = app.test_client().get("/console/api/swagger.json").get_json()
|
||||
payload = app.test_client().get("/console/api/openapi.json").get_json()
|
||||
operation = payload["paths"]["/account/avatar"]["get"]
|
||||
params = _parameters_by_name(operation)
|
||||
|
||||
|
||||
Generated
+3
-7
@@ -1630,7 +1630,7 @@ requires-dist = [
|
||||
{ name = "flask-login", specifier = "==0.6.3" },
|
||||
{ name = "flask-migrate", specifier = ">=4.1.0,<5.0.0" },
|
||||
{ name = "flask-orjson", specifier = ">=2.0.0,<3.0.0" },
|
||||
{ name = "flask-restx", specifier = ">=1.3.2,<2.0.0" },
|
||||
{ name = "flask-restx", git = "https://github.com/asukaminato0721/flask-restx?rev=27758e26f8f740d7525d5039c51a9e524b6e2b68" },
|
||||
{ name = "gevent", specifier = ">=26.4.0,<26.5.0" },
|
||||
{ name = "gevent-websocket", specifier = "==0.10.1" },
|
||||
{ name = "gmpy2", specifier = ">=2.3.0,<3.0.0" },
|
||||
@@ -2570,8 +2570,8 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "flask-restx"
|
||||
version = "1.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
version = "1.3.3.dev0"
|
||||
source = { git = "https://github.com/asukaminato0721/flask-restx?rev=27758e26f8f740d7525d5039c51a9e524b6e2b68#27758e26f8f740d7525d5039c51a9e524b6e2b68" }
|
||||
dependencies = [
|
||||
{ name = "aniso8601" },
|
||||
{ name = "flask" },
|
||||
@@ -2580,10 +2580,6 @@ dependencies = [
|
||||
{ name = "referencing" },
|
||||
{ name = "werkzeug" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/89/9b9ca58cbb8e9ec46f4a510ba93878e0c88d518bf03c350e3b1b7ad85cbe/flask-restx-1.3.2.tar.gz", hash = "sha256:0ae13d77e7d7e4dce513970cfa9db45364aef210e99022de26d2b73eb4dbced5", size = 2814719, upload-time = "2025-09-23T20:34:25.21Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/3f/b82cd8e733a355db1abb8297afbf59ec972c00ef90bf8d4eed287958b204/flask_restx-1.3.2-py2.py3-none-any.whl", hash = "sha256:6e035496e8223668044fc45bf769e526352fd648d9e159bd631d94fd645a687b", size = 2799859, upload-time = "2025-09-23T20:34:23.055Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flask-sqlalchemy"
|
||||
|
||||
Reference in New Issue
Block a user