feat(openapi): generate stainless config "more" programmatically (#4164)

Generate the Stainless client config directly from code so we can
validate the config before we ever write the YAML.

This change enforces allowed HTTP verbs/paths, detects duplicate routes
across resources, and ensures README example endpoints exist and match
the OpenAPI spec. The generator now fails fast when config entries
drift, keeping the published config (hopefully) more current with the
spec. I think more validation can be done but this is a good start.
This commit is contained in:
Ashwin Bharambe 2025-11-17 12:48:03 -08:00 committed by GitHub
parent 5fe6098350
commit 7d3db6b22c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 990 additions and 193 deletions

View file

@ -11,6 +11,13 @@ This module provides functionality to generate OpenAPI specifications
from FastAPI applications.
"""
from .main import generate_openapi_spec, main
__all__ = ["generate_openapi_spec", "main"]
def __getattr__(name: str):
if name in {"generate_openapi_spec", "main"}:
from .main import generate_openapi_spec as _gos
from .main import main as _main
return {"generate_openapi_spec": _gos, "main": _main}[name]
raise AttributeError(name)

View file

@ -0,0 +1,7 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
# Package marker for Stainless config generation.

View file

@ -0,0 +1,821 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.
from __future__ import annotations
from collections.abc import Iterator
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
import yaml
HEADER = "# yaml-language-server: $schema=https://app.stainlessapi.com/config-internal.schema.json\n\n"
SECTION_ORDER = [
"organization",
"security",
"security_schemes",
"targets",
"client_settings",
"environments",
"pagination",
"settings",
"openapi",
"readme",
"resources",
]
ORGANIZATION = {
"name": "llama-stack-client",
"docs": "https://llama-stack.readthedocs.io/en/latest/",
"contact": "llamastack@meta.com",
}
SECURITY = [{}, {"BearerAuth": []}]
SECURITY_SCHEMES = {"BearerAuth": {"type": "http", "scheme": "bearer"}}
TARGETS = {
"node": {
"package_name": "llama-stack-client",
"production_repo": "llamastack/llama-stack-client-typescript",
"publish": {"npm": False},
},
"python": {
"package_name": "llama_stack_client",
"production_repo": "llamastack/llama-stack-client-python",
"options": {"use_uv": True},
"publish": {"pypi": True},
"project_name": "llama_stack_client",
},
"kotlin": {
"reverse_domain": "com.llama_stack_client.api",
"production_repo": None,
"publish": {"maven": False},
},
"go": {
"package_name": "llama-stack-client",
"production_repo": "llamastack/llama-stack-client-go",
"options": {"enable_v2": True, "back_compat_use_shared_package": False},
},
}
CLIENT_SETTINGS = {
"default_env_prefix": "LLAMA_STACK_CLIENT",
"opts": {
"api_key": {
"type": "string",
"read_env": "LLAMA_STACK_CLIENT_API_KEY",
"auth": {"security_scheme": "BearerAuth"},
"nullable": True,
}
},
}
ENVIRONMENTS = {"production": "http://any-hosted-llama-stack.com"}
PAGINATION = [
{
"name": "datasets_iterrows",
"type": "offset",
"request": {
"dataset_id": {"type": "string"},
"start_index": {
"type": "integer",
"x-stainless-pagination-property": {"purpose": "offset_count_param"},
},
"limit": {"type": "integer"},
},
"response": {
"data": {"type": "array", "items": {"type": "object"}},
"next_index": {
"type": "integer",
"x-stainless-pagination-property": {"purpose": "offset_count_start_field"},
},
},
},
{
"name": "openai_cursor_page",
"type": "cursor",
"request": {
"limit": {"type": "integer"},
"after": {
"type": "string",
"x-stainless-pagination-property": {"purpose": "next_cursor_param"},
},
},
"response": {
"data": {"type": "array", "items": {}},
"has_more": {"type": "boolean"},
"last_id": {
"type": "string",
"x-stainless-pagination-property": {"purpose": "next_cursor_field"},
},
},
},
]
SETTINGS = {
"license": "MIT",
"unwrap_response_fields": ["data"],
"file_header": "Copyright (c) Meta Platforms, Inc. and affiliates.\n"
"All rights reserved.\n"
"\n"
"This source code is licensed under the terms described in the "
"LICENSE file in\n"
"the root directory of this source tree.\n",
}
OPENAPI = {
"transformations": [
{
"command": "mergeObject",
"reason": "Better return_type using enum",
"args": {
"target": ["$.components.schemas"],
"object": {
"ReturnType": {
"additionalProperties": False,
"properties": {
"type": {
"enum": [
"string",
"number",
"boolean",
"array",
"object",
"json",
"union",
"chat_completion_input",
"completion_input",
"agent_turn_input",
]
}
},
"required": ["type"],
"type": "object",
}
},
},
},
{
"command": "replaceProperties",
"reason": "Replace return type properties with better model (see above)",
"args": {
"filter": {
"only": [
"$.components.schemas.ScoringFn.properties.return_type",
"$.components.schemas.RegisterScoringFunctionRequest.properties.return_type",
]
},
"value": {"$ref": "#/components/schemas/ReturnType"},
},
},
{
"command": "oneOfToAnyOf",
"reason": "Prism (mock server) doesn't like one of our "
"requests as it technically matches multiple "
"variants",
},
]
}
README = {
"example_requests": {
"default": {
"type": "request",
"endpoint": "post /v1/chat/completions",
"params": {},
},
"headline": {"type": "request", "endpoint": "get /v1/models", "params": {}},
"pagination": {
"type": "request",
"endpoint": "post /v1/chat/completions",
"params": {},
},
}
}
ALL_RESOURCES = {
"$shared": {
"models": {
"interleaved_content_item": "InterleavedContentItem",
"interleaved_content": "InterleavedContent",
"param_type": "ParamType",
"safety_violation": "SafetyViolation",
"sampling_params": "SamplingParams",
"scoring_result": "ScoringResult",
"system_message": "SystemMessage",
}
},
"toolgroups": {
"models": {
"tool_group": "ToolGroup",
"list_tool_groups_response": "ListToolGroupsResponse",
},
"methods": {
"register": "post /v1/toolgroups",
"get": "get /v1/toolgroups/{toolgroup_id}",
"list": "get /v1/toolgroups",
"unregister": "delete /v1/toolgroups/{toolgroup_id}",
},
},
"tools": {
"methods": {
"get": "get /v1/tools/{tool_name}",
"list": {"paginated": False, "endpoint": "get /v1/tools"},
}
},
"tool_runtime": {
"models": {
"tool_def": "ToolDef",
"tool_invocation_result": "ToolInvocationResult",
},
"methods": {
"list_tools": {
"paginated": False,
"endpoint": "get /v1/tool-runtime/list-tools",
},
"invoke_tool": "post /v1/tool-runtime/invoke",
},
},
"responses": {
"models": {
"response_object_stream": "OpenAIResponseObjectStream",
"response_object": "OpenAIResponseObject",
},
"methods": {
"create": {
"type": "http",
"streaming": {
"stream_event_model": "responses.response_object_stream",
"param_discriminator": "stream",
},
"endpoint": "post /v1/responses",
},
"retrieve": "get /v1/responses/{response_id}",
"list": {"type": "http", "endpoint": "get /v1/responses"},
"delete": {
"type": "http",
"endpoint": "delete /v1/responses/{response_id}",
},
},
"subresources": {
"input_items": {
"methods": {
"list": {
"type": "http",
"paginated": False,
"endpoint": "get /v1/responses/{response_id}/input_items",
}
}
}
},
},
"prompts": {
"models": {"prompt": "Prompt", "list_prompts_response": "ListPromptsResponse"},
"methods": {
"create": "post /v1/prompts",
"list": {"paginated": False, "endpoint": "get /v1/prompts"},
"retrieve": "get /v1/prompts/{prompt_id}",
"update": "post /v1/prompts/{prompt_id}",
"delete": "delete /v1/prompts/{prompt_id}",
"set_default_version": "post /v1/prompts/{prompt_id}/set-default-version",
},
"subresources": {
"versions": {
"methods": {
"list": {
"paginated": False,
"endpoint": "get /v1/prompts/{prompt_id}/versions",
}
}
}
},
},
"conversations": {
"models": {"conversation_object": "Conversation"},
"methods": {
"create": {"type": "http", "endpoint": "post /v1/conversations"},
"retrieve": "get /v1/conversations/{conversation_id}",
"update": {
"type": "http",
"endpoint": "post /v1/conversations/{conversation_id}",
},
"delete": {
"type": "http",
"endpoint": "delete /v1/conversations/{conversation_id}",
},
},
"subresources": {
"items": {
"methods": {
"get": {
"type": "http",
"endpoint": "get /v1/conversations/{conversation_id}/items/{item_id}",
},
"list": {
"type": "http",
"endpoint": "get /v1/conversations/{conversation_id}/items",
},
"create": {
"type": "http",
"endpoint": "post /v1/conversations/{conversation_id}/items",
},
"delete": {
"type": "http",
"endpoint": "delete /v1/conversations/{conversation_id}/items/{item_id}",
},
}
}
},
},
"inspect": {
"models": {
"healthInfo": "HealthInfo",
"providerInfo": "ProviderInfo",
"routeInfo": "RouteInfo",
"versionInfo": "VersionInfo",
},
"methods": {"health": "get /v1/health", "version": "get /v1/version"},
},
"embeddings": {
"models": {"create_embeddings_response": "OpenAIEmbeddingsResponse"},
"methods": {"create": "post /v1/embeddings"},
},
"chat": {
"models": {"chat_completion_chunk": "OpenAIChatCompletionChunk"},
"subresources": {
"completions": {
"methods": {
"create": {
"type": "http",
"streaming": {
"stream_event_model": "chat.chat_completion_chunk",
"param_discriminator": "stream",
},
"endpoint": "post /v1/chat/completions",
},
"list": {
"type": "http",
"paginated": False,
"endpoint": "get /v1/chat/completions",
},
"retrieve": {
"type": "http",
"endpoint": "get /v1/chat/completions/{completion_id}",
},
}
}
},
},
"completions": {
"methods": {
"create": {
"type": "http",
"streaming": {"param_discriminator": "stream"},
"endpoint": "post /v1/completions",
}
}
},
"vector_io": {
"models": {"queryChunksResponse": "QueryChunksResponse"},
"methods": {
"insert": "post /v1/vector-io/insert",
"query": "post /v1/vector-io/query",
},
},
"vector_stores": {
"models": {
"vector_store": "VectorStoreObject",
"list_vector_stores_response": "VectorStoreListResponse",
"vector_store_delete_response": "VectorStoreDeleteResponse",
"vector_store_search_response": "VectorStoreSearchResponsePage",
},
"methods": {
"create": "post /v1/vector_stores",
"list": "get /v1/vector_stores",
"retrieve": "get /v1/vector_stores/{vector_store_id}",
"update": "post /v1/vector_stores/{vector_store_id}",
"delete": "delete /v1/vector_stores/{vector_store_id}",
"search": "post /v1/vector_stores/{vector_store_id}/search",
},
"subresources": {
"files": {
"models": {"vector_store_file": "VectorStoreFileObject"},
"methods": {
"list": "get /v1/vector_stores/{vector_store_id}/files",
"retrieve": "get /v1/vector_stores/{vector_store_id}/files/{file_id}",
"update": "post /v1/vector_stores/{vector_store_id}/files/{file_id}",
"delete": "delete /v1/vector_stores/{vector_store_id}/files/{file_id}",
"create": "post /v1/vector_stores/{vector_store_id}/files",
"content": "get /v1/vector_stores/{vector_store_id}/files/{file_id}/content",
},
},
"file_batches": {
"models": {
"vector_store_file_batches": "VectorStoreFileBatchObject",
"list_vector_store_files_in_batch_response": "VectorStoreFilesListInBatchResponse",
},
"methods": {
"create": "post /v1/vector_stores/{vector_store_id}/file_batches",
"retrieve": "get /v1/vector_stores/{vector_store_id}/file_batches/{batch_id}",
"list_files": "get /v1/vector_stores/{vector_store_id}/file_batches/{batch_id}/files",
"cancel": "post /v1/vector_stores/{vector_store_id}/file_batches/{batch_id}/cancel",
},
},
},
},
"models": {
"models": {
"model": "OpenAIModel",
"list_models_response": "OpenAIListModelsResponse",
},
"methods": {
"list": {"paginated": False, "endpoint": "get /v1/models"},
"retrieve": "get /v1/models/{model_id}",
"register": "post /v1/models",
"unregister": "delete /v1/models/{model_id}",
},
"subresources": {"openai": {"methods": {"list": {"paginated": False, "endpoint": "get /v1/models"}}}},
},
"providers": {
"models": {"list_providers_response": "ListProvidersResponse"},
"methods": {
"list": {"paginated": False, "endpoint": "get /v1/providers"},
"retrieve": "get /v1/providers/{provider_id}",
},
},
"routes": {
"models": {"list_routes_response": "ListRoutesResponse"},
"methods": {"list": {"paginated": False, "endpoint": "get /v1/inspect/routes"}},
},
"moderations": {
"models": {"create_response": "ModerationObject"},
"methods": {"create": "post /v1/moderations"},
},
"safety": {
"models": {"run_shield_response": "RunShieldResponse"},
"methods": {"run_shield": "post /v1/safety/run-shield"},
},
"shields": {
"models": {"shield": "Shield", "list_shields_response": "ListShieldsResponse"},
"methods": {
"retrieve": "get /v1/shields/{identifier}",
"list": {"paginated": False, "endpoint": "get /v1/shields"},
"register": "post /v1/shields",
"delete": "delete /v1/shields/{identifier}",
},
},
"scoring": {
"methods": {
"score": "post /v1/scoring/score",
"score_batch": "post /v1/scoring/score-batch",
}
},
"scoring_functions": {
"models": {
"scoring_fn": "ScoringFn",
"scoring_fn_params": "ScoringFnParams",
"list_scoring_functions_response": "ListScoringFunctionsResponse",
},
"methods": {
"retrieve": "get /v1/scoring-functions/{scoring_fn_id}",
"list": {"paginated": False, "endpoint": "get /v1/scoring-functions"},
"register": "post /v1/scoring-functions",
"unregister": "delete /v1/scoring-functions/{scoring_fn_id}",
},
},
"files": {
"models": {
"file": "OpenAIFileObject",
"list_files_response": "ListOpenAIFileResponse",
"delete_file_response": "OpenAIFileDeleteResponse",
},
"methods": {
"create": "post /v1/files",
"list": "get /v1/files",
"retrieve": "get /v1/files/{file_id}",
"delete": "delete /v1/files/{file_id}",
"content": "get /v1/files/{file_id}/content",
},
},
"batches": {
"methods": {
"create": "post /v1/batches",
"list": "get /v1/batches",
"retrieve": "get /v1/batches/{batch_id}",
"cancel": "post /v1/batches/{batch_id}/cancel",
}
},
"alpha": {
"subresources": {
"inference": {"methods": {"rerank": "post /v1alpha/inference/rerank"}},
"post_training": {
"models": {
"algorithm_config": "AlgorithmConfig",
"post_training_job": "PostTrainingJob",
"list_post_training_jobs_response": "ListPostTrainingJobsResponse",
},
"methods": {
"preference_optimize": "post /v1alpha/post-training/preference-optimize",
"supervised_fine_tune": "post /v1alpha/post-training/supervised-fine-tune",
},
"subresources": {
"job": {
"methods": {
"artifacts": "get /v1alpha/post-training/job/artifacts",
"cancel": "post /v1alpha/post-training/job/cancel",
"status": "get /v1alpha/post-training/job/status",
"list": {
"paginated": False,
"endpoint": "get /v1alpha/post-training/jobs",
},
}
}
},
},
"benchmarks": {
"models": {
"benchmark": "Benchmark",
"list_benchmarks_response": "ListBenchmarksResponse",
},
"methods": {
"retrieve": "get /v1alpha/eval/benchmarks/{benchmark_id}",
"list": {
"paginated": False,
"endpoint": "get /v1alpha/eval/benchmarks",
},
"register": "post /v1alpha/eval/benchmarks",
"unregister": "delete /v1alpha/eval/benchmarks/{benchmark_id}",
},
},
"eval": {
"models": {
"evaluate_response": "EvaluateResponse",
"benchmark_config": "BenchmarkConfig",
"job": "Job",
},
"methods": {
"evaluate_rows": "post /v1alpha/eval/benchmarks/{benchmark_id}/evaluations",
"run_eval": "post /v1alpha/eval/benchmarks/{benchmark_id}/jobs",
"evaluate_rows_alpha": "post /v1alpha/eval/benchmarks/{benchmark_id}/evaluations",
"run_eval_alpha": "post /v1alpha/eval/benchmarks/{benchmark_id}/jobs",
},
"subresources": {
"jobs": {
"methods": {
"cancel": "delete /v1alpha/eval/benchmarks/{benchmark_id}/jobs/{job_id}",
"status": "get /v1alpha/eval/benchmarks/{benchmark_id}/jobs/{job_id}",
"retrieve": "get /v1alpha/eval/benchmarks/{benchmark_id}/jobs/{job_id}/result",
}
}
},
},
}
},
"beta": {
"subresources": {
"datasets": {
"models": {"list_datasets_response": "ListDatasetsResponse"},
"methods": {
"register": "post /v1beta/datasets",
"retrieve": "get /v1beta/datasets/{dataset_id}",
"list": {"paginated": False, "endpoint": "get /v1beta/datasets"},
"unregister": "delete /v1beta/datasets/{dataset_id}",
"iterrows": "get /v1beta/datasetio/iterrows/{dataset_id}",
"appendrows": "post /v1beta/datasetio/append-rows/{dataset_id}",
},
}
}
},
}
HTTP_METHODS = {"get", "post", "put", "patch", "delete", "options", "head"}
@dataclass
class Endpoint:
method: str
path: str
extra: dict[str, Any] = field(default_factory=dict)
@classmethod
def from_config(cls, value: Any) -> Endpoint:
if isinstance(value, str):
method, _, path = value.partition(" ")
return cls._from_parts(method, path)
if isinstance(value, dict) and "endpoint" in value:
method, _, path = value["endpoint"].partition(" ")
extra = {k: v for k, v in value.items() if k != "endpoint"}
endpoint = cls._from_parts(method, path)
endpoint.extra.update(extra)
return endpoint
raise ValueError(f"Unsupported endpoint value: {value!r}")
@classmethod
def _from_parts(cls, method: str, path: str) -> Endpoint:
method = method.strip().lower()
path = path.strip()
if method not in HTTP_METHODS:
raise ValueError(f"Unsupported HTTP method for Stainless config: {method!r}")
if not path.startswith("/"):
raise ValueError(f"Endpoint path must start with '/': {path!r}")
return cls(method=method, path=path)
def to_config(self) -> Any:
if not self.extra:
return f"{self.method} {self.path}"
data = dict(self.extra)
data["endpoint"] = f"{self.method} {self.path}"
return data
def route_key(self) -> str:
return f"{self.method} {self.path}"
@dataclass
class Resource:
models: dict[str, str] | None = None
methods: dict[str, Endpoint] = field(default_factory=dict)
subresources: dict[str, Resource] = field(default_factory=dict)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> Resource:
models = data.get("models")
methods = {name: Endpoint.from_config(value) for name, value in data.get("methods", {}).items()}
subresources = {name: cls.from_dict(value) for name, value in data.get("subresources", {}).items()}
return cls(models=models, methods=methods, subresources=subresources)
def to_config(self) -> dict[str, Any]:
result: dict[str, Any] = {}
if self.models:
result["models"] = self.models
if self.methods:
result["methods"] = {name: endpoint.to_config() for name, endpoint in self.methods.items()}
if self.subresources:
result["subresources"] = {name: resource.to_config() for name, resource in self.subresources.items()}
return result
def collect_endpoint_paths(self) -> set[str]:
paths = {endpoint.route_key() for endpoint in self.methods.values()}
for subresource in self.subresources.values():
paths.update(subresource.collect_endpoint_paths())
return paths
def iter_endpoints(self, prefix: str) -> Iterator[tuple[str, str]]:
for method_name, endpoint in self.methods.items():
label = f"{prefix}.{method_name}" if prefix else method_name
yield endpoint.route_key(), label
for sub_name, subresource in self.subresources.items():
sub_prefix = f"{prefix}.{sub_name}" if prefix else sub_name
yield from subresource.iter_endpoints(sub_prefix)
_RESOURCES = {name: Resource.from_dict(data) for name, data in ALL_RESOURCES.items()}
def _load_openapi_paths(openapi_path: Path) -> set[str]:
spec = yaml.safe_load(openapi_path.read_text()) or {}
paths: set[str] = set()
for path, path_item in (spec.get("paths") or {}).items():
if not isinstance(path_item, dict):
continue
for method, operation in path_item.items():
if not isinstance(operation, dict):
continue
paths.add(f"{str(method).lower()} {path}")
return paths
@dataclass(frozen=True)
class StainlessConfig:
organization: dict[str, Any]
security: list[Any]
security_schemes: dict[str, Any]
targets: dict[str, Any]
client_settings: dict[str, Any]
environments: dict[str, Any]
pagination: list[dict[str, Any]]
settings: dict[str, Any]
openapi: dict[str, Any]
readme: dict[str, Any]
resources: dict[str, Resource]
@classmethod
def make(cls) -> StainlessConfig:
return cls(
organization=ORGANIZATION,
security=SECURITY,
security_schemes=SECURITY_SCHEMES,
targets=TARGETS,
client_settings=CLIENT_SETTINGS,
environments=ENVIRONMENTS,
pagination=PAGINATION,
settings=SETTINGS,
openapi=OPENAPI,
readme=README,
resources=dict(_RESOURCES),
)
def referenced_paths(self) -> set[str]:
paths: set[str] = set()
for resource in self.resources.values():
paths.update(resource.collect_endpoint_paths())
paths.update(self.readme_endpoint_paths())
return paths
def readme_endpoint_paths(self) -> set[str]:
example_requests = self.readme.get("example_requests", {}) if self.readme else {}
paths: set[str] = set()
for entry in example_requests.values():
endpoint = entry.get("endpoint") if isinstance(entry, dict) else None
if isinstance(endpoint, str):
method, _, route = endpoint.partition(" ")
method = method.strip().lower()
route = route.strip()
if method and route:
paths.add(f"{method} {route}")
return paths
def endpoint_map(self) -> dict[str, list[str]]:
mapping: dict[str, list[str]] = {}
for resource_name, resource in self.resources.items():
for route, label in resource.iter_endpoints(resource_name):
mapping.setdefault(route, []).append(label)
return mapping
def validate_unique_endpoints(self) -> None:
duplicates: dict[str, list[str]] = {}
for route, labels in self.endpoint_map().items():
top_levels = {label.split(".", 1)[0] for label in labels}
if len(top_levels) > 1:
duplicates[route] = labels
if duplicates:
formatted = "\n".join(
f" - {route} defined in: {', '.join(sorted(labels))}" for route, labels in sorted(duplicates.items())
)
raise ValueError("Duplicate endpoints found across resources:\n" + formatted)
def validate_readme_endpoints(self) -> None:
resource_paths: set[str] = set()
for resource in self.resources.values():
resource_paths.update(resource.collect_endpoint_paths())
missing = sorted(path for path in self.readme_endpoint_paths() if path not in resource_paths)
if missing:
formatted = "\n".join(f" - {path}" for path in missing)
raise ValueError("README example endpoints are not present in Stainless resources:\n" + formatted)
def to_dict(self) -> dict[str, Any]:
cfg: dict[str, Any] = {}
for section in SECTION_ORDER:
if section == "resources":
cfg[section] = {name: resource.to_config() for name, resource in self.resources.items()}
continue
cfg[section] = getattr(self, section)
return cfg
def validate_against_openapi(self, openapi_path: Path) -> None:
if not openapi_path.exists():
raise FileNotFoundError(f"OpenAPI spec not found at {openapi_path}")
spec_paths = _load_openapi_paths(openapi_path)
config_paths = self.referenced_paths()
missing = sorted(path for path in config_paths if path not in spec_paths)
if missing:
formatted = "\n".join(f" - {path}" for path in missing)
raise ValueError("Stainless config references missing endpoints:\n" + formatted)
def validate(self, openapi_path: Path | None = None) -> None:
self.validate_unique_endpoints()
self.validate_readme_endpoints()
if openapi_path is not None:
self.validate_against_openapi(openapi_path)
def build_config() -> dict[str, Any]:
return StainlessConfig.make().to_dict()
def write_config(repo_root: Path, openapi_path: Path | None = None) -> Path:
stainless_config = StainlessConfig.make()
spec_path = (openapi_path or (repo_root / "client-sdks" / "stainless" / "openapi.yml")).resolve()
stainless_config.validate(spec_path)
yaml_text = yaml.safe_dump(stainless_config.to_dict(), sort_keys=False)
output = repo_root / "client-sdks" / "stainless" / "config.yml"
output.write_text(HEADER + yaml_text)
return output
def main() -> None:
repo_root = Path(__file__).resolve().parents[3]
output = write_config(repo_root)
print(f"Wrote Stainless config: {output}")
if __name__ == "__main__":
main()