# /// 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()