864 lines
30 KiB
Python
864 lines
30 KiB
Python
# /// script
|
|
# dependencies = ["texttable"]
|
|
# ///
|
|
"""Generate RST documentation from protobuf JSON definitions."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import re
|
|
from enum import Enum
|
|
from pathlib import Path
|
|
from textwrap import dedent
|
|
from typing import Any
|
|
|
|
from texttable import Texttable
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _gen_break() -> str:
|
|
return "\n\n===========================\n\n"
|
|
|
|
|
|
def _gen_h1(link_id: str, title: str) -> str:
|
|
return f"""
|
|
|
|
.. _{link_id}:
|
|
|
|
{title}
|
|
{"=" * len(title)}
|
|
|
|
"""
|
|
|
|
|
|
def _gen_h2(link_id: str, title: str) -> str:
|
|
return f"""
|
|
|
|
.. _{link_id}:
|
|
|
|
{title}
|
|
{"-" * len(title)}
|
|
|
|
"""
|
|
|
|
|
|
def _gen_page_title(title: str) -> str:
|
|
link = title.lower().replace(" ", "-")
|
|
header = "=" * len(title)
|
|
return f"""
|
|
.. _{link}:
|
|
|
|
{header}
|
|
{title}
|
|
{header}
|
|
|
|
"""
|
|
|
|
|
|
def _validation_error(msg: str) -> str:
|
|
return f"JSON Validation Error: {msg}"
|
|
|
|
|
|
def _validate_doc_public_json(docjson: dict[str, Any]) -> None:
|
|
_logger.info("Validating doc_public.json file.")
|
|
if "files" not in docjson:
|
|
_logger.error(docjson.keys())
|
|
raise ValueError(_validation_error("No 'files' key"))
|
|
files = docjson["files"][0]
|
|
_logger.info("Checking 'content'")
|
|
if "content" not in files:
|
|
_logger.error(files.keys())
|
|
raise ValueError(_validation_error("No 'content' key"))
|
|
content = files["content"][0]
|
|
_logger.info("Checking 'message', 'service', 'enum'")
|
|
for key in ("message", "service", "enum"):
|
|
if key not in content:
|
|
_logger.error(content.keys())
|
|
raise ValueError(_validation_error(f"No '{key}' key"))
|
|
_logger.info("Structure Appears to be valid! Continuing...")
|
|
|
|
|
|
class MsgType(Enum):
|
|
GENERIC = 1
|
|
REQUEST = 2
|
|
RESPONSE = 3
|
|
|
|
|
|
def _gen_id(full_path: list[str]) -> str:
|
|
return "".join(full_path)
|
|
|
|
|
|
class Field:
|
|
"""A field within a protobuf message, containing name, type, and description."""
|
|
|
|
def __init__(self, full_path: list[str], name: str, description: str, field_type: str) -> None:
|
|
_logger.debug(f"Creating Field {name}")
|
|
self.id = _gen_id(full_path)
|
|
self.name = name
|
|
self.description = description
|
|
self.field_type = field_type
|
|
|
|
def __repr__(self) -> str:
|
|
return self.name
|
|
|
|
@staticmethod
|
|
def table_header() -> list[str]:
|
|
return ["Field Name", "Type", "Description"]
|
|
|
|
def to_table(self) -> list[str]:
|
|
return [self.name, self.field_type, self.description]
|
|
|
|
@staticmethod
|
|
def _parse_name(field_details: dict[str, Any]) -> str:
|
|
declared_type = field_details["field_type"]
|
|
if declared_type == "oneof":
|
|
names = [f"``{x['field_name']}``" for x in field_details["oneof"]]
|
|
return " OR ".join(names)
|
|
return field_details["field_name"]
|
|
|
|
@staticmethod
|
|
def _parse_type(field_details: dict[str, Any]) -> str:
|
|
declared_type = field_details["field_type"]
|
|
if declared_type == "oneof":
|
|
field_types = [Field._convert_to_link(x["field_type"]) for x in field_details["oneof"]]
|
|
return " OR ".join(field_types)
|
|
return Field._convert_to_link(field_details["field_type"])
|
|
|
|
@staticmethod
|
|
def _normalize_description(text: str) -> str:
|
|
"""Normalize whitespace in description text to avoid RST indentation issues."""
|
|
# Split into lines, strip leading/trailing whitespace from each, rejoin
|
|
lines = text.split("\n")
|
|
normalized_lines = [line.strip() for line in lines]
|
|
return " ".join(line for line in normalized_lines if line)
|
|
|
|
@staticmethod
|
|
def _parse_description(field_details: dict[str, Any]) -> str:
|
|
declared_type = field_details["field_type"]
|
|
deprecated = " This field is deprecated." if field_details["deprecated"] else ""
|
|
required = " This field is required." if field_details["validate_required"] else ""
|
|
|
|
if declared_type == "oneof":
|
|
|
|
def to_lowercase_first_char(s: str) -> str:
|
|
return s[:1].lower() + s[1:] if s else ""
|
|
|
|
options = []
|
|
for name, obj in zip(
|
|
Field._parse_name(field_details).split(" OR "), field_details["oneof"]
|
|
):
|
|
desc = Field._normalize_description(obj["description"])
|
|
options.append(f"If {name}, {to_lowercase_first_char(desc)}")
|
|
return " ".join(options) + required + deprecated
|
|
|
|
desc = Field._normalize_description(field_details["description"])
|
|
return desc + required + deprecated
|
|
|
|
@staticmethod
|
|
def _convert_to_link(raw_string: str) -> str:
|
|
# Only create internal refs for mlflow types, not external ones
|
|
if "." in raw_string:
|
|
if raw_string.startswith("mlflow."):
|
|
return f":ref:`{raw_string.replace('.', '').lower()}`"
|
|
# External types (google.protobuf.*, opentelemetry.*, etc.)
|
|
return f"``{raw_string}``"
|
|
return f"``{raw_string}``"
|
|
|
|
@classmethod
|
|
def parse_all_from(cls, field_list: list[dict[str, Any]]) -> list[Field]:
|
|
all_instances = []
|
|
for field in field_list:
|
|
full_path = field["full_path"]
|
|
# Skip deprecated fields
|
|
if field["deprecated"]:
|
|
continue
|
|
name = Field._parse_name(field)
|
|
field_type = Field._parse_type(field)
|
|
description = Field._parse_description(field)
|
|
if field["repeated"]:
|
|
field_type = "An array of " + field_type
|
|
if field["visibility"] == "public":
|
|
all_instances.append(cls(full_path, name, description, field_type))
|
|
return all_instances
|
|
|
|
|
|
class Value:
|
|
"""An enum value within a ProtoEnum."""
|
|
|
|
def __init__(self, full_path: list[str], name: str, description: str) -> None:
|
|
self.id = _gen_id(full_path)
|
|
self.name = name
|
|
self.description = description
|
|
|
|
def __repr__(self) -> str:
|
|
return self.name
|
|
|
|
@staticmethod
|
|
def table_header() -> list[str]:
|
|
return ["Name", "Description"]
|
|
|
|
def to_table(self) -> list[str]:
|
|
return [self.name, self.description]
|
|
|
|
@classmethod
|
|
def parse_all_from(cls, value_list: list[dict[str, Any]]) -> list[Value]:
|
|
return [cls(v["full_path"], v["value"], v["description"]) for v in value_list]
|
|
|
|
|
|
class ProtoEnum:
|
|
"""A protobuf enum with a series of Values."""
|
|
|
|
def __init__(
|
|
self, full_path: list[str], name: str, description: str, values: list[Value]
|
|
) -> None:
|
|
self.id = _gen_id(full_path)
|
|
self.name = name
|
|
self.description = description
|
|
self.values = values
|
|
|
|
def __repr__(self) -> str:
|
|
return f"{self.id}\n {self.name}"
|
|
|
|
def _generate_values_table(self) -> str:
|
|
tbl = Texttable(max_width=200)
|
|
header = Value.table_header()
|
|
tbl.add_rows([header] + [f.to_table() for f in self.values])
|
|
return tbl.draw()
|
|
|
|
def to_rst(self) -> str:
|
|
values = self._generate_values_table()
|
|
title = _gen_h2(self.id, self.name)
|
|
section = f"\n{self.description}\n\n{values}"
|
|
return title + section
|
|
|
|
@classmethod
|
|
def parse_all_from(cls, files: list[dict[str, Any]]) -> list[ProtoEnum]:
|
|
all_instances = []
|
|
for proto_file in files:
|
|
# Top-level enums
|
|
enums = [x["enum"] for x in proto_file["content"] if x["enum"]]
|
|
# Enums inside of messages
|
|
for content in proto_file["content"]:
|
|
if content["message"]:
|
|
enums += content["message"].get("enums") or []
|
|
for enum in enums:
|
|
values = Value.parse_all_from(enum["values"])
|
|
all_instances.append(
|
|
cls(enum["full_path"], enum["name"], enum["description"], values)
|
|
)
|
|
return all_instances
|
|
|
|
|
|
class Message:
|
|
"""A protobuf message containing fields."""
|
|
|
|
def __init__(
|
|
self, full_path: list[str], name: str, description: str, fields: list[Field]
|
|
) -> None:
|
|
_logger.debug(f"Creating Message: {name}")
|
|
self.id = _gen_id(full_path)
|
|
self.name = name
|
|
self.description = description
|
|
self.fields = fields
|
|
self.type = MsgType.GENERIC
|
|
|
|
def __repr__(self) -> str:
|
|
return self.id
|
|
|
|
def _generate_field_table(self) -> str:
|
|
tbl = Texttable(max_width=200)
|
|
header = [Field.table_header()]
|
|
non_empty_fields = [f for f in self.fields if f.name]
|
|
rows = [f.to_table() for f in non_empty_fields]
|
|
tbl.add_rows(header + rows)
|
|
return tbl.draw()
|
|
|
|
def _generate_rst_title(self) -> str:
|
|
if self.type == MsgType.REQUEST:
|
|
return _gen_h2(self.id, "Request Structure")
|
|
elif self.type == MsgType.RESPONSE:
|
|
return _gen_h2(self.id, "Response Structure")
|
|
return _gen_h2(self.id, self.name)
|
|
|
|
def to_rst(self) -> str:
|
|
if not self.fields:
|
|
return ""
|
|
fields = self._generate_field_table()
|
|
title = self._generate_rst_title()
|
|
section = f"\n\n{self.description}\n\n\n{fields}"
|
|
return title + section
|
|
|
|
@classmethod
|
|
def parse_all_from_list(cls, message_list: list[dict[str, Any]]) -> list[Message]:
|
|
all_instances = []
|
|
for msg in message_list:
|
|
if msg["visibility"] != "public":
|
|
continue
|
|
fields = Field.parse_all_from(msg["fields"])
|
|
all_instances.append(cls(msg["full_path"], msg["name"], msg["description"], fields))
|
|
if msg["messages"]:
|
|
all_instances.extend(cls.parse_all_from_list(msg["messages"]))
|
|
return all_instances
|
|
|
|
@classmethod
|
|
def parse_all_from(cls, files: list[dict[str, Any]]) -> list[Message]:
|
|
all_instances = []
|
|
for proto_file in files:
|
|
for content in proto_file["content"]:
|
|
if not content["message"]:
|
|
continue
|
|
message = content["message"]
|
|
if message["visibility"] != "public":
|
|
continue
|
|
fields = Field.parse_all_from(message["fields"])
|
|
all_instances.append(
|
|
cls(message["full_path"], message["name"], message["description"], fields)
|
|
)
|
|
if message["messages"]:
|
|
all_instances.extend(cls.parse_all_from_list(message["messages"]))
|
|
return all_instances
|
|
|
|
|
|
class Method:
|
|
"""An RPC method within a service, containing request and response messages."""
|
|
|
|
def __init__(
|
|
self,
|
|
full_path: list[str],
|
|
name: str,
|
|
description: str,
|
|
path: str,
|
|
method: str,
|
|
request: list[str],
|
|
response: list[str],
|
|
title: str | None,
|
|
api_version: str | None = None,
|
|
) -> None:
|
|
self.id = _gen_id(full_path)
|
|
self.name = name
|
|
self.description = description
|
|
self.path = path
|
|
self.method = method
|
|
self.request = _gen_id(request)
|
|
self.response = _gen_id(response)
|
|
self.request_message: Message | None = None
|
|
self.response_message: Message | None = None
|
|
self.api_version: str | None = api_version
|
|
self.title = title
|
|
|
|
@classmethod
|
|
def parse_all_from(cls, method_list: list[dict[str, Any]]) -> list[Method]:
|
|
all_instances = []
|
|
for m in method_list:
|
|
rpc_options = m["rpc_options"]
|
|
if rpc_options["visibility"] != "public":
|
|
continue
|
|
since_major = rpc_options.get("since_major")
|
|
since_minor = rpc_options.get("since_minor")
|
|
method_api_version = None
|
|
if since_major is not None and since_minor is not None:
|
|
method_api_version = f"{since_major}.{since_minor}"
|
|
all_instances.append(
|
|
cls(
|
|
full_path=m["full_path"],
|
|
name=m["name"],
|
|
description=m["description"],
|
|
path=rpc_options["path"],
|
|
method=rpc_options["method"],
|
|
request=m["request_full_path"],
|
|
response=m["response_full_path"],
|
|
title=rpc_options.get("rpc_doc_title"),
|
|
api_version=method_api_version,
|
|
)
|
|
)
|
|
return all_instances
|
|
|
|
def __repr__(self) -> str:
|
|
reqm = "HasMsg" if self.request_message else "NoMsg"
|
|
resm = "HasMsg" if self.response_message else "NoMsg"
|
|
return f"{self.name}, {self.request} ({reqm}) -> {self.response} ({resm})"
|
|
|
|
def to_rst(self) -> str:
|
|
if not self.api_version:
|
|
raise ValueError("API version must be set before generating RST")
|
|
prepped_title = self.title or " ".join(re.split(r"\W+", self.path)[2:]).title().lstrip()
|
|
title = _gen_h1(self.id, prepped_title)
|
|
tbl = Texttable(max_width=200)
|
|
tbl.add_rows([
|
|
["Endpoint", "HTTP Method"],
|
|
[f"``{self.api_version}{self.path}``", f"``{self.method}``"],
|
|
])
|
|
parameters = tbl.draw()
|
|
body = f"""
|
|
{parameters}
|
|
|
|
{self.description}
|
|
"""
|
|
ret_value = _gen_break() + title + body + "\n\n"
|
|
if self.request_message:
|
|
ret_value += self.request_message.to_rst()
|
|
if self.response_message:
|
|
ret_value += self.response_message.to_rst()
|
|
return ret_value
|
|
|
|
|
|
class Service:
|
|
"""A protobuf service containing RPC methods."""
|
|
|
|
def __init__(
|
|
self, full_path: list[str], name: str, description: str, methods: list[Method]
|
|
) -> None:
|
|
self.id = _gen_id(full_path)
|
|
self.name = name
|
|
self.description = description
|
|
self.methods = methods
|
|
|
|
@classmethod
|
|
def parse_all_from(cls, files: list[dict[str, Any]]) -> list[Service]:
|
|
all_instances = []
|
|
for proto_file in files:
|
|
for content in proto_file["content"]:
|
|
if not content["service"]:
|
|
continue
|
|
service = content["service"]
|
|
methods = Method.parse_all_from(service["methods"])
|
|
all_instances.append(
|
|
cls(service["full_path"], service["name"], service["description"], methods)
|
|
)
|
|
return all_instances
|
|
|
|
def __repr__(self) -> str:
|
|
method_strs = "\n ".join(str(m) for m in self.methods)
|
|
return f"{self.name}\n Methods: {method_strs}"
|
|
|
|
def to_rst(self, method_order: list[str] | None = None) -> str:
|
|
sorted_methods = sorted(self.methods, key=lambda x: x.name)
|
|
if method_order:
|
|
method_map = {name: idx for idx, name in enumerate(method_order)}
|
|
sorted_methods = sorted(
|
|
self.methods, key=lambda x: method_map.get(x.request, len(method_order))
|
|
)
|
|
return "".join(method.to_rst() for method in sorted_methods)
|
|
|
|
|
|
class API:
|
|
"""Main API class for generating REST API documentation."""
|
|
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
description: str,
|
|
api_version: str,
|
|
dst_path: Path,
|
|
valid_proto_files: list[str],
|
|
) -> None:
|
|
self.name = name
|
|
self.description = description
|
|
self.dst_path = dst_path
|
|
self.valid_proto_files = valid_proto_files
|
|
self.services: list[Service] | None = None
|
|
self.messages: list[Message] | None = None
|
|
self.enums: list[ProtoEnum] | None = None
|
|
self.api_version = api_version
|
|
|
|
def __repr__(self) -> str:
|
|
return self.name
|
|
|
|
def _file_filter(self, proto_file: dict[str, Any]) -> bool:
|
|
return proto_file["filename"] in self.valid_proto_files
|
|
|
|
def _validate_proto_list(self, proto_list: list[dict[str, Any]], context: str) -> None:
|
|
if not proto_list or len(proto_list) != len(self.valid_proto_files):
|
|
_logger.error("Length mismatch. This is likely due to a name error")
|
|
for f in self.valid_proto_files:
|
|
_logger.error(f"Valid Proto File: {f}")
|
|
for f in proto_list:
|
|
_logger.error(f"Actual Proto File: {f}")
|
|
_logger.error("Maybe someone changed the name/location of a proto file?")
|
|
raise ValueError(f"Proto file mismatch in {context}")
|
|
|
|
def set_services(
|
|
self, proto_file_list: list[dict[str, Any]], service_order: list[str] | None = None
|
|
) -> None:
|
|
_logger.debug("Starting service generation")
|
|
_logger.debug(f"Starts with total of: {len(proto_file_list)}")
|
|
services_proto_list = [f for f in proto_file_list if self._file_filter(f)]
|
|
self._validate_proto_list(services_proto_list, "set_services")
|
|
services = Service.parse_all_from(services_proto_list)
|
|
if service_order:
|
|
order_map = {name: idx for idx, name in enumerate(service_order)}
|
|
default_order = len(service_order)
|
|
services = sorted(
|
|
services, key=lambda x: (order_map.get(x.name, default_order), x.name)
|
|
)
|
|
else:
|
|
services = sorted(services, key=lambda x: x.name)
|
|
self.services = services
|
|
_logger.debug("Completed service generation")
|
|
|
|
def set_messages(self, proto_file_list: list[dict[str, Any]]) -> None:
|
|
_logger.debug("Starting message generation")
|
|
_logger.debug(f"Starts with total of: {len(proto_file_list)}")
|
|
messages_proto_list = [f for f in proto_file_list if self._file_filter(f)]
|
|
self._validate_proto_list(messages_proto_list, "set_messages")
|
|
self.messages = sorted(Message.parse_all_from(messages_proto_list), key=lambda x: x.name)
|
|
_logger.debug("Completed message generation")
|
|
|
|
def set_enums(self, proto_file_list: list[dict[str, Any]]) -> None:
|
|
_logger.debug("Starting enum generation")
|
|
_logger.debug(f"Starts with total of: {len(proto_file_list)}")
|
|
enums_proto_list = [f for f in proto_file_list if self._file_filter(f)]
|
|
self._validate_proto_list(enums_proto_list, "set_enums")
|
|
self.enums = sorted(ProtoEnum.parse_all_from(enums_proto_list), key=lambda x: x.name)
|
|
_logger.debug("Completed enum generation")
|
|
|
|
def connect_methods_messages(self) -> None:
|
|
"""Connect request/response messages to their corresponding methods."""
|
|
for service in self.services:
|
|
for method in service.methods:
|
|
request_set = False
|
|
response_set = False
|
|
for message in self.messages:
|
|
if message.id == method.request and not request_set:
|
|
method.request_message = message
|
|
message.type = MsgType.REQUEST
|
|
request_set = True
|
|
_logger.debug(f"Set Request Message for {method}")
|
|
elif message.id == method.response and not response_set:
|
|
method.response_message = message
|
|
response_set = True
|
|
message.type = MsgType.RESPONSE
|
|
_logger.debug(f"Set Response Message for {method}")
|
|
|
|
if not request_set:
|
|
_logger.warning(f"Request not set {method} for {self}")
|
|
if not response_set:
|
|
_logger.warning(f"Response not set {method} for {self}")
|
|
# Use per-method api_version from proto "since" when set, else API default
|
|
if method.api_version is None:
|
|
method.api_version = self.api_version
|
|
|
|
def set_all(
|
|
self, proto_file_list: list[dict[str, Any]], service_order: list[str] | None = None
|
|
) -> None:
|
|
_logger.info(f"Setting Services for {self.name}")
|
|
self.set_services(proto_file_list, service_order)
|
|
_logger.info(f"Finished Setting Services for {self.name}")
|
|
_logger.info(f"Setting Messages for {self.name}")
|
|
self.set_messages(proto_file_list)
|
|
_logger.info(f"Finished Setting Messages for {self.name}")
|
|
_logger.info(f"Setting Enums for {self.name}")
|
|
self.set_enums(proto_file_list)
|
|
_logger.info(f"Finished Setting Enums for {self.name}")
|
|
_logger.info(f"Connecting Messages -> Services for {self.name}")
|
|
self.connect_methods_messages()
|
|
_logger.info(f"Finished Connecting Messages -> Services under {self.name} API")
|
|
|
|
def write_rst(self, method_order: list[str] | None = None) -> None:
|
|
if self.services is None or self.messages is None or self.enums is None:
|
|
raise ValueError("Must call set_all() before write_rst()")
|
|
if not self.services or not self.messages:
|
|
_logger.error(
|
|
f"Services: {len(self.services)} Messages: {len(self.messages)} "
|
|
f"Enums: {len(self.enums)}"
|
|
)
|
|
raise ValueError("No services or messages found - check doc_public.json")
|
|
|
|
services_rst = [s.to_rst(method_order) for s in self.services]
|
|
enums_rst = [s.to_rst() for s in self.enums]
|
|
generic_messages_rst = [s.to_rst() for s in self.messages if s.type == MsgType.GENERIC]
|
|
|
|
with self.dst_path.open("w") as f:
|
|
f.write(_gen_page_title(f"{self.name} API"))
|
|
f.write(self.description)
|
|
f.write("\n.. contents:: Table of Contents\n :local:\n :depth: 1")
|
|
f.write("".join(services_rst))
|
|
f.write(_gen_h1(self.name + "add", "Data Structures"))
|
|
f.write("".join(generic_messages_rst))
|
|
f.write("".join(enums_rst))
|
|
f.write("\n")
|
|
|
|
|
|
# Valid MLflow message names for documentation ordering
|
|
VALID_MLFLOW_MESSAGES = [
|
|
# ===== Experiments =====
|
|
"mlflowCreateExperiment",
|
|
"mlflowSearchExperiments",
|
|
"mlflowGetExperiment",
|
|
"mlflowGetExperimentByName",
|
|
"mlflowDeleteExperiment",
|
|
"mlflowRestoreExperiment",
|
|
"mlflowUpdateExperiment",
|
|
"mlflowSetExperimentTag",
|
|
"mlflowDeleteExperimentTag",
|
|
# ===== Runs =====
|
|
"mlflowCreateRun",
|
|
"mlflowUpdateRun",
|
|
"mlflowDeleteRun",
|
|
"mlflowRestoreRun",
|
|
"mlflowGetRun",
|
|
"mlflowSearchRuns",
|
|
"mlflowLogMetric",
|
|
"mlflowLogParam",
|
|
"mlflowLogBatch",
|
|
"mlflowLogModel",
|
|
"mlflowLogInputs",
|
|
"mlflowLogOutputs",
|
|
"mlflowSetTag",
|
|
"mlflowDeleteTag",
|
|
"mlflowGetMetricHistory",
|
|
"mlflowGetMetricHistoryBulkInterval",
|
|
"mlflowListArtifacts",
|
|
# ===== Model Registry =====
|
|
"mlflowCreateRegisteredModel",
|
|
"mlflowGetRegisteredModel",
|
|
"mlflowRenameRegisteredModel",
|
|
"mlflowUpdateRegisteredModel",
|
|
"mlflowDeleteRegisteredModel",
|
|
"mlflowSearchRegisteredModels",
|
|
"mlflowGetLatestVersions",
|
|
"mlflowCreateModelVersion",
|
|
"mlflowGetModelVersion",
|
|
"mlflowUpdateModelVersion",
|
|
"mlflowDeleteModelVersion",
|
|
"mlflowSearchModelVersions",
|
|
"mlflowGetModelVersionDownloadUri",
|
|
"mlflowTransitionModelVersionStage",
|
|
"mlflowSetRegisteredModelTag",
|
|
"mlflowSetModelVersionTag",
|
|
"mlflowDeleteRegisteredModelTag",
|
|
"mlflowDeleteModelVersionTag",
|
|
"mlflowSetRegisteredModelAlias",
|
|
"mlflowDeleteRegisteredModelAlias",
|
|
"mlflowGetModelVersionByAlias",
|
|
# ===== Traces =====
|
|
"mlflowStartTrace",
|
|
"mlflowEndTrace",
|
|
"mlflowGetTraceInfo",
|
|
"mlflowGetTraceInfoV3",
|
|
"mlflowBatchGetTraces",
|
|
"mlflowGetTrace",
|
|
"mlflowSearchTraces",
|
|
"mlflowSearchTracesV3",
|
|
"mlflowSearchUnifiedTraces",
|
|
"mlflowGetOnlineTraceDetails",
|
|
"mlflowDeleteTraces",
|
|
"mlflowDeleteTracesV3",
|
|
"mlflowSetTraceTag",
|
|
"mlflowSetTraceTagV3",
|
|
"mlflowDeleteTraceTag",
|
|
"mlflowDeleteTraceTagV3",
|
|
"mlflowStartTraceV3",
|
|
"mlflowLinkTracesToRun",
|
|
"mlflowLinkPromptsToTrace",
|
|
"mlflowCalculateTraceFilterCorrelation",
|
|
"mlflowQueryTraceMetrics",
|
|
# ===== Assessments =====
|
|
"mlflowCreateAssessment",
|
|
"mlflowUpdateAssessment",
|
|
"mlflowDeleteAssessment",
|
|
"mlflowGetAssessmentRequest",
|
|
# ===== Datasets =====
|
|
"mlflowSearchDatasets",
|
|
"mlflowCreateDataset",
|
|
"mlflowGetDataset",
|
|
"mlflowDeleteDataset",
|
|
"mlflowSearchEvaluationDatasets",
|
|
"mlflowSetDatasetTags",
|
|
"mlflowDeleteDatasetTag",
|
|
"mlflowUpsertDatasetRecords",
|
|
"mlflowGetDatasetExperimentIds",
|
|
"mlflowGetDatasetRecords",
|
|
"mlflowAddDatasetToExperiments",
|
|
"mlflowRemoveDatasetFromExperiments",
|
|
# ===== Logged Models =====
|
|
"mlflowCreateLoggedModel",
|
|
"mlflowFinalizeLoggedModel",
|
|
"mlflowGetLoggedModel",
|
|
"mlflowDeleteLoggedModel",
|
|
"mlflowSearchLoggedModels",
|
|
"mlflowSetLoggedModelTags",
|
|
"mlflowDeleteLoggedModelTag",
|
|
"mlflowListLoggedModelArtifacts",
|
|
"mlflowLogLoggedModelParamsRequest",
|
|
# ===== Scorers =====
|
|
"mlflowRegisterScorer",
|
|
"mlflowListScorers",
|
|
"mlflowListScorerVersions",
|
|
"mlflowGetScorer",
|
|
"mlflowDeleteScorer",
|
|
# ===== Gateway =====
|
|
"mlflowCreateGatewaySecret",
|
|
"mlflowGetGatewaySecretInfo",
|
|
"mlflowUpdateGatewaySecret",
|
|
"mlflowDeleteGatewaySecret",
|
|
"mlflowListGatewaySecretInfos",
|
|
"mlflowCreateGatewayModelDefinition",
|
|
"mlflowGetGatewayModelDefinition",
|
|
"mlflowListGatewayModelDefinitions",
|
|
"mlflowUpdateGatewayModelDefinition",
|
|
"mlflowDeleteGatewayModelDefinition",
|
|
"mlflowCreateGatewayEndpoint",
|
|
"mlflowGetGatewayEndpoint",
|
|
"mlflowUpdateGatewayEndpoint",
|
|
"mlflowDeleteGatewayEndpoint",
|
|
"mlflowListGatewayEndpoints",
|
|
"mlflowAttachModelToGatewayEndpoint",
|
|
"mlflowDetachModelFromGatewayEndpoint",
|
|
"mlflowCreateGatewayEndpointBinding",
|
|
"mlflowDeleteGatewayEndpointBinding",
|
|
"mlflowListGatewayEndpointBindings",
|
|
"mlflowSetGatewayEndpointTag",
|
|
"mlflowDeleteGatewayEndpointTag",
|
|
"mlflowGetSecretsConfig",
|
|
# ===== Prompt Optimization =====
|
|
"mlflowCreatePromptOptimizationJob",
|
|
"mlflowGetPromptOptimizationJob",
|
|
"mlflowSearchPromptOptimizationJobs",
|
|
"mlflowCancelPromptOptimizationJob",
|
|
"mlflowDeletePromptOptimizationJob",
|
|
# ===== Webhooks =====
|
|
"mlflowCreateWebhook",
|
|
"mlflowListWebhooks",
|
|
"mlflowGetWebhook",
|
|
"mlflowUpdateWebhook",
|
|
"mlflowDeleteWebhook",
|
|
"mlflowTestWebhook",
|
|
# ===== Artifacts (mlflow.artifacts package) =====
|
|
"mlflowartifactsDownloadArtifact",
|
|
"mlflowartifactsUploadArtifact",
|
|
"mlflowartifactsListArtifacts",
|
|
"mlflowartifactsDeleteArtifact",
|
|
"mlflowartifactsCreateMultipartUpload",
|
|
"mlflowartifactsCompleteMultipartUpload",
|
|
"mlflowartifactsAbortMultipartUpload",
|
|
# ===== Data Types =====
|
|
"mlflowExperiment",
|
|
"mlflowRun",
|
|
"mlflowRunInfo",
|
|
"mlflowRunTag",
|
|
"mlflowExperimentTag",
|
|
"mlflowRunData",
|
|
"mlflowRunInputs",
|
|
"mlflowRunOutputs",
|
|
"mlflowMetric",
|
|
"mlflowParam",
|
|
"mlflowFileInfo",
|
|
"mlflowDatasetInput",
|
|
"mlflowDataset",
|
|
"mlflowInputTag",
|
|
"mlflowModelInput",
|
|
"mlflowModelOutput",
|
|
"mlflowRegisteredModel",
|
|
"mlflowModelVersion",
|
|
"mlflowRegisteredModelTag",
|
|
"mlflowModelVersionTag",
|
|
"mlflowRegisteredModelAlias",
|
|
"mlflowModelParam",
|
|
"mlflowModelMetric",
|
|
"mlflowDeploymentJobConnection",
|
|
"mlflowModelVersionDeploymentJobState",
|
|
"mlflowTraceInfo",
|
|
"mlflowTraceInfoV3",
|
|
"mlflowTrace",
|
|
"mlflowTraceLocation",
|
|
"mlflowTraceRequestMetadata",
|
|
"mlflowTraceTag",
|
|
"mlflowMetricAggregation",
|
|
"mlflowMetricDataPoint",
|
|
"mlflowDatasetSummary",
|
|
"mlflowLoggedModel",
|
|
"mlflowLoggedModelInfo",
|
|
"mlflowLoggedModelTag",
|
|
"mlflowLoggedModelRegistrationInfo",
|
|
"mlflowLoggedModelData",
|
|
"mlflowLoggedModelParameter",
|
|
"mlflowScorer",
|
|
"mlflowGatewaySecretInfo",
|
|
"mlflowGatewayModelDefinition",
|
|
"mlflowGatewayEndpointModelMapping",
|
|
"mlflowGatewayEndpoint",
|
|
"mlflowGatewayEndpointTag",
|
|
"mlflowGatewayEndpointBinding",
|
|
"mlflowFallbackConfig",
|
|
"mlflowGatewayEndpointModelConfig",
|
|
"mlflowAssessmentSource",
|
|
"mlflowAssessmentError",
|
|
"mlflowExpectation",
|
|
"mlflowFeedback",
|
|
"mlflowAssessment",
|
|
"mlflowWebhookEvent",
|
|
"mlflowWebhook",
|
|
"mlflowWebhookTestResult",
|
|
"mlflowJobState",
|
|
"mlflowPromptOptimizationJobTag",
|
|
"mlflowPromptOptimizationJobConfig",
|
|
"mlflowPromptOptimizationJob",
|
|
"mlflowartifactsFileInfo",
|
|
"mlflowartifactsMultipartUploadCredential",
|
|
"mlflowartifactsMultipartUploadPart",
|
|
"mlflowMetricWithRunId",
|
|
]
|
|
|
|
MLFLOW_PROTOS = [
|
|
"service.proto",
|
|
"model_registry.proto",
|
|
"webhooks.proto",
|
|
"mlflow_artifacts.proto",
|
|
"assessments.proto",
|
|
"datasets.proto",
|
|
"jobs.proto",
|
|
"prompt_optimization.proto",
|
|
]
|
|
|
|
# Order of services in documentation (services not listed will be sorted alphabetically at the end)
|
|
SERVICE_ORDER = [
|
|
"MlflowService",
|
|
"ModelRegistryService",
|
|
"WebhookService",
|
|
"MlflowArtifactsService",
|
|
]
|
|
|
|
MLFLOW_DESCRIPTION = dedent("""
|
|
The MLflow REST API allows you to create, list, and get experiments and runs, and log
|
|
parameters, metrics, and artifacts. The API is hosted under the ``/api`` route on the MLflow
|
|
tracking server. For example, to search for experiments on a tracking server hosted at
|
|
``http://localhost:5000``, make a POST request to ``http://localhost:5000/api/2.0/mlflow/experiments/search``.
|
|
|
|
.. important::
|
|
The MLflow REST API requires content type ``application/json`` for all POST requests.
|
|
""")
|
|
|
|
|
|
def main() -> None:
|
|
logging.basicConfig(format="%(levelname)s:%(lineno)d:%(message)s", level=logging.INFO)
|
|
|
|
src = Path("mlflow/protos/protos.json")
|
|
dst = Path("docs/api_reference/source/rest-api.rst")
|
|
api_version = "2.0"
|
|
|
|
_logger.info(f"API VERSION: {api_version}")
|
|
_logger.info(f"Reading Source: {src}")
|
|
|
|
with src.open() as f:
|
|
docjson = json.load(f)
|
|
|
|
_validate_doc_public_json(docjson)
|
|
proto_files = docjson["files"]
|
|
|
|
mlflow_api = API(
|
|
name="REST",
|
|
description=MLFLOW_DESCRIPTION,
|
|
api_version=api_version,
|
|
dst_path=dst,
|
|
valid_proto_files=MLFLOW_PROTOS,
|
|
)
|
|
mlflow_api.set_all(proto_files, SERVICE_ORDER)
|
|
mlflow_api.write_rst(VALID_MLFLOW_MESSAGES)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|