Files
opensearch-pyd/utils/generate_api.py
T

770 lines
27 KiB
Python
Raw Normal View History

2019-12-17 22:15:52 +01:00
#!/usr/bin/env python
# SPDX-License-Identifier: Apache-2.0
#
# The OpenSearch Contributors require contributions made to
# this file be licensed under the Apache-2.0 license or a
# compatible open source license.
#
# Modifications Copyright OpenSearch Contributors. See
# GitHub history for details.
#
# Modifications Copyright OpenSearch Contributors. See
# GitHub history for details.
#
2023-11-21 13:04:39 -05:00
# Licensed to Elasticsearch b.V. under one or more contributor
# license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright
2023-11-21 13:04:39 -05:00
# ownership. Elasticsearch b.V. licenses this file to you under
# the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
2020-04-23 11:20:33 -05:00
import json
import os
2019-12-17 22:15:52 +01:00
import re
2020-04-23 11:20:33 -05:00
from functools import lru_cache
from itertools import chain, groupby
from operator import itemgetter
from pathlib import Path
from typing import Any, Dict
2019-12-17 22:15:52 +01:00
import black
import deepmerge
import requests
import unasync
import urllib3
2019-12-17 22:15:52 +01:00
from click.testing import CliRunner
from jinja2 import Environment, FileSystemLoader, TemplateNotFound, select_autoescape
2019-12-17 22:15:52 +01:00
2020-04-23 11:20:33 -05:00
http = urllib3.PoolManager()
2019-12-17 22:15:52 +01:00
# line to look for in the original source file
SEPARATOR = " # AUTO-GENERATED-API-DEFINITIONS #"
# global substitutions for python keywords
SUBSTITUTIONS = {"type": "doc_type", "from": "from_"}
2019-12-17 22:15:52 +01:00
# api path(s)
2020-04-23 11:20:33 -05:00
BRANCH_NAME = "7.x"
2019-12-22 14:47:42 +01:00
CODE_ROOT = Path(__file__).absolute().parent.parent
GLOBAL_QUERY_PARAMS = {
"pretty": "Optional[bool]",
"human": "Optional[bool]",
"error_trace": "Optional[bool]",
"format": "Optional[str]",
"filter_path": "Optional[Union[str, Collection[str]]]",
"request_timeout": "Optional[Union[int, float]]",
"ignore": "Optional[Union[int, Collection[int]]]",
"opaque_id": "Optional[str]",
"http_auth": "Optional[Union[str, Tuple[str, str]]]",
"api_key": "Optional[Union[str, Tuple[str, str]]]",
}
2019-12-17 22:15:52 +01:00
jinja_env = Environment(
autoescape=select_autoescape(["html", "xml"]),
2019-12-22 15:15:00 +01:00
loader=FileSystemLoader([CODE_ROOT / "utils" / "templates"]),
2019-12-17 22:15:52 +01:00
trim_blocks=True,
lstrip_blocks=True,
)
def blacken(filename: Any) -> None:
2024-01-19 13:36:05 -05:00
"""
runs 'black' https://pypi.org/project/black/ on the given file
:param filename: file to reformant
"""
2019-12-17 22:15:52 +01:00
runner = CliRunner()
result = runner.invoke(black.main, [str(filename)])
assert result.exit_code == 0, result.output
2020-04-23 11:20:33 -05:00
@lru_cache()
def is_valid_url(url: str) -> bool:
2024-01-19 13:36:05 -05:00
"""
makes a call to the url
:param url: url to check
:return: True if status code is between HTTP 200 inclusive and 400 exclusive; False otherwise
"""
return 200 <= http.request("HEAD", url).status < 400
2020-04-23 11:20:33 -05:00
2019-12-17 22:15:52 +01:00
class Module:
def __init__(self, namespace: str) -> None:
self.namespace: Any = namespace
self._apis: Any = []
2019-12-17 22:15:52 +01:00
self.parse_orig()
def add(self, api: Any) -> None:
2024-01-19 13:36:05 -05:00
"""
add an API to the list of modules
:param api: an API object
"""
2019-12-17 22:15:52 +01:00
self._apis.append(api)
def parse_orig(self) -> None:
2024-01-19 13:36:05 -05:00
"""
reads the written module and updates with important code specific to this client
"""
2019-12-17 22:15:52 +01:00
self.orders = []
2023-11-06 13:08:19 -05:00
self.header = "from typing import Any, Collection, Optional, Tuple, Union\n\n"
namespace_new = "".join(word.capitalize() for word in self.namespace.split("_"))
2023-11-06 13:08:19 -05:00
self.header += "class " + namespace_new + "Client(NamespacedClient):"
2020-05-15 09:36:47 -05:00
if os.path.exists(self.filepath):
2024-01-19 13:36:05 -05:00
with open(self.filepath, encoding="utf-8") as file:
content = file.read()
2019-12-17 22:15:52 +01:00
header_lines = []
for line in content.split("\n"):
header_lines.append(line)
if line == SEPARATOR:
break
# no separator found
else:
header_lines = []
for line in content.split("\n"):
header_lines.append(line)
2019-12-22 15:15:00 +01:00
if line.startswith("class"):
2023-11-06 13:08:19 -05:00
if "security.py" in str(self.filepath):
# TODO: FIXME, import code
header_lines.append(
2024-01-19 13:36:05 -05:00
" from ._patch import health_check, update_audit_config # type: ignore" # pylint: disable=line-too-long
)
2019-12-17 22:15:52 +01:00
break
self.header = "\n".join(header_lines)
2020-03-13 13:44:46 -05:00
self.orders = re.findall(
2020-06-24 14:25:28 -05:00
r"\n (?:async )?def ([a-z_]+)\(", content, re.MULTILINE
2019-12-17 22:15:52 +01:00
)
def _position(self, api: Any) -> Any:
2019-12-17 22:15:52 +01:00
try:
return self.orders.index(api.name)
except ValueError:
return len(self.orders)
2023-11-06 13:08:19 -05:00
def sort(self) -> None:
2024-01-19 13:36:05 -05:00
"""
sorts the list of APIs by the Module._position key
"""
2019-12-17 22:15:52 +01:00
self._apis.sort(key=self._position)
2023-11-06 13:08:19 -05:00
def dump(self) -> None:
2024-01-19 13:36:05 -05:00
"""
writes the module out to disk
"""
2019-12-17 22:15:52 +01:00
self.sort()
2024-01-19 13:36:05 -05:00
# This code snippet adds headers to each generated module indicating
# that the code is generated.The separator is the last line in the
# "THIS CODE IS AUTOMATICALLY GENERATED" header.
header_separator = "# -----------------------------------------------------------------------------------------+" # pylint: disable=line-too-long
2023-11-21 13:04:39 -05:00
license_header_end_1 = "# GitHub history for details."
license_header_end_2 = "# under the License."
update_header = True
2023-11-21 13:04:39 -05:00
license_position = 0
2023-11-21 13:04:39 -05:00
# Identifying the insertion point for the "THIS CODE IS AUTOMATICALLY GENERATED" header.
if os.path.exists(self.filepath):
2024-01-19 13:36:05 -05:00
with open(self.filepath, "r", encoding="utf-8") as file:
content = file.read()
if header_separator in content:
update_header = False
header_end_position = (
content.find(header_separator) + len(header_separator) + 2
)
header_position = content.rfind("\n", 0, header_end_position) + 1
2023-11-21 13:04:39 -05:00
if license_header_end_1 in content:
if license_header_end_2 in content:
position = (
2023-11-21 13:04:39 -05:00
content.find(license_header_end_2)
+ len(license_header_end_2)
+ 2
)
else:
position = (
2023-11-21 13:04:39 -05:00
content.find(license_header_end_1)
+ len(license_header_end_1)
+ 2
)
2023-11-21 13:04:39 -05:00
license_position = content.rfind("\n", 0, position) + 1
current_script_folder = os.path.dirname(os.path.abspath(__file__))
generated_file_header_path = os.path.join(
current_script_folder, "generated_file_headers.txt"
)
2024-01-19 13:36:05 -05:00
with open(generated_file_header_path, "r", encoding="utf-8") as header_file:
header_content = header_file.read()
2024-01-19 13:36:05 -05:00
# Imports are temporarily removed from the header and are regenerated
# later to ensure imports are updated after code generation.
self.header = "\n".join(
line for line in self.header.split("\n") if "from .utils import" not in line
)
2024-01-19 13:36:05 -05:00
with open(self.filepath, "w", encoding="utf-8") as file:
if update_header is True:
2024-01-19 13:36:05 -05:00
file.write(
2023-11-21 13:04:39 -05:00
self.header[:license_position]
+ "\n"
+ header_content
+ "\n\n"
+ "#replace_token#\n"
2023-11-21 13:04:39 -05:00
+ self.header[license_position:]
)
else:
2024-01-19 13:36:05 -05:00
file.write(
self.header[:header_position]
+ "\n"
+ "#replace_token#\n"
+ self.header[header_position:]
)
2019-12-17 22:15:52 +01:00
for api in self._apis:
2024-01-19 13:36:05 -05:00
file.write(api.to_python())
# Generating imports for each module
utils_imports = ""
file_content = ""
2024-01-19 13:36:05 -05:00
with open(self.filepath, "r", encoding="utf-8") as file:
content = file.read()
keywords = [
"SKIP_IN_PATH",
"_normalize_hosts",
"_escape",
"_make_path",
"query_params",
"_bulk_body",
"_base64_auth_header",
"NamespacedClient",
"AddonClient",
]
present_keywords = [keyword for keyword in keywords if keyword in content]
if present_keywords:
utils_imports = "from .utils import"
result = f"{utils_imports} {', '.join(present_keywords)}"
utils_imports = result
file_content = content.replace("#replace_token#", utils_imports)
2024-01-19 13:36:05 -05:00
with open(self.filepath, "w", encoding="utf-8") as file:
file.write(file_content)
2020-05-15 09:36:47 -05:00
@property
def filepath(self) -> Any:
2024-01-19 13:36:05 -05:00
"""
:return: absolute path to the module
"""
2023-11-06 13:08:19 -05:00
return CODE_ROOT / f"opensearchpy/_async/client/{self.namespace}.py"
2019-12-17 22:15:52 +01:00
class API:
def __init__(self, namespace: str, name: str, definition: Any) -> None:
2019-12-17 22:15:52 +01:00
self.namespace = namespace
self.name = name
# overwrite the dict to maintain key order
definition["params"] = {
SUBSTITUTIONS.get(p, p): v for p, v in definition.get("params", {}).items()
}
self._def = definition
self.description = ""
self.doc_url = ""
self.stability = self._def.get("stability", "stable")
self.deprecation_message = self._def.get("deprecation_message")
2019-12-17 22:15:52 +01:00
if isinstance(definition["documentation"], str):
self.doc_url = definition["documentation"]
else:
2021-05-25 14:44:41 +02:00
# set as attribute so it may be overridden by Module.add
2020-07-29 15:51:19 -05:00
self.description = (
definition["documentation"].get("description", "").strip()
)
2019-12-17 22:15:52 +01:00
self.doc_url = definition["documentation"].get("url", "")
2020-03-13 13:44:46 -05:00
# Filter out bad URL refs like 'TODO'
# and serve all docs over HTTPS.
if self.doc_url:
if not self.doc_url.startswith("http"):
self.doc_url = ""
if self.doc_url.startswith("http://"):
self.doc_url = self.doc_url.replace("http://", "https://")
2020-04-23 11:20:33 -05:00
# Try setting doc refs like 'current' and 'master' to our branches ref.
if BRANCH_NAME is not None:
2020-06-24 14:25:28 -05:00
revised_url = re.sub(
2021-09-16 14:59:29 +05:30
"/opensearchpy/reference/[^/]+/",
f"/opensearchpy/reference/{BRANCH_NAME}/",
2020-06-24 14:25:28 -05:00
self.doc_url,
)
2020-04-23 11:20:33 -05:00
if is_valid_url(revised_url):
self.doc_url = revised_url
else:
print(f"URL {revised_url!r}, falling back on {self.doc_url!r}")
@property
def all_parts(self) -> Dict[str, str]:
2024-01-19 13:36:05 -05:00
"""
updates the url parts from the specification
:return: dict of updated parts
"""
2019-12-17 22:15:52 +01:00
parts = {}
for url in self._def["url"]["paths"]:
parts.update(url.get("parts", {}))
2024-01-19 13:36:05 -05:00
for part in parts:
if "required" not in parts[part]:
parts[part]["required"] = all(
part in url.get("parts", {}) for url in self._def["url"]["paths"]
)
2024-01-19 13:36:05 -05:00
parts[part]["type"] = "Any"
2019-12-17 22:15:52 +01:00
# This piece of logic corresponds to calling
# client.tasks.get() w/o a task_id which was erroneously
# allowed in the 7.1 client library. This functionality
# is deprecated and will be removed in 8.x.
if self.namespace == "tasks" and self.name == "get":
parts["task_id"]["required"] = False
2019-12-17 22:15:52 +01:00
for k, sub in SUBSTITUTIONS.items():
if k in parts:
parts[sub] = parts.pop(k)
2024-01-25 18:17:09 -05:00
_, components = self.url_parts
2019-12-17 22:15:52 +01:00
def ind(item: Any) -> Any:
2019-12-17 22:15:52 +01:00
try:
return components.index(item[0])
except ValueError:
return len(components)
parts = dict(sorted(parts.items(), key=ind))
return parts
@property
def params(self) -> Any:
2024-01-19 13:36:05 -05:00
"""
:return: itertools.chain of required parts of the API
"""
parts = self.all_parts
2020-03-20 14:21:11 +01:00
params = self._def.get("params", {})
2019-12-17 22:15:52 +01:00
return chain(
((p, parts[p]) for p in parts if parts[p]["required"]), # type: ignore
2020-03-20 14:21:11 +01:00
(("body", self.body),) if self.body else (),
2020-06-24 14:25:28 -05:00
(
(p, parts[p])
for p in parts
if not parts[p]["required"] and p not in params # type: ignore
2020-06-24 14:25:28 -05:00
),
2020-03-20 14:21:11 +01:00
sorted(params.items(), key=lambda x: (x[0] not in parts, x[0])),
2019-12-17 22:15:52 +01:00
)
@property
def body(self) -> Any:
2024-01-19 13:36:05 -05:00
"""
:return: body of the API spec
"""
body_api_spec = self._def.get("body", {})
if body_api_spec:
body_api_spec.setdefault("required", False)
return body_api_spec
2019-12-17 22:15:52 +01:00
@property
def query_params(self) -> Any:
2024-01-19 13:36:05 -05:00
"""
:return: any query string parameters from the specification
"""
2019-12-22 15:15:00 +01:00
return (
2024-01-19 13:36:05 -05:00
key
for key in sorted(self._def.get("params", {}).keys())
if key not in self.all_parts
2019-12-22 15:15:00 +01:00
)
2019-12-17 22:15:52 +01:00
@property
def all_func_params(self) -> Any:
2024-01-19 13:36:05 -05:00
"""
Parameters that will be in the '@query_params' decorator list
and parameters that will be in the function signature.
"""
params = list(self._def.get("params", {}).keys())
for url in self._def["url"]["paths"]:
params.extend(url.get("parts", {}).keys())
if self.body:
params.append("body")
return params
2019-12-17 22:15:52 +01:00
@property
def path(self) -> Any:
2024-01-19 13:36:05 -05:00
"""
:return: the first lexically ordered path in url.paths
"""
2019-12-17 22:15:52 +01:00
return max(
(path for path in self._def["url"]["paths"]),
key=lambda p: len(re.findall(r"\{([^}]+)\}", p["path"])),
)
@property
def method(self) -> Any:
2024-01-19 13:36:05 -05:00
"""
To adhere to the HTTP RFC we shouldn't send
bodies in GET requests.
:return: an updated HTTP method to use to communicate with the OpenSearch API
"""
2020-03-13 13:44:46 -05:00
default_method = self.path["methods"][0]
if self.name == "refresh" or self.name == "flush":
return "POST"
2020-03-13 13:44:46 -05:00
if self.body and default_method == "GET" and "POST" in self.path["methods"]:
return "POST"
if "POST" and "PUT" in self.path["methods"] and self.name != "bulk":
return "PUT"
2020-03-13 13:44:46 -05:00
return default_method
2019-12-17 22:15:52 +01:00
@property
def url_parts(self) -> Any:
2024-01-19 13:36:05 -05:00
"""
:return tuple of boolean (if the path is dynamic), list of url parts
"""
2019-12-17 22:15:52 +01:00
path = self.path["path"]
dynamic = "{" in path
if not dynamic:
return dynamic, path
parts = []
for part in path.split("/"):
if not part:
continue
if part[0] == "{":
part = part[1:-1]
parts.append(SUBSTITUTIONS.get(part, part))
else:
parts.append(f"'{part}'")
return dynamic, parts
@property
def required_parts(self) -> Any:
2024-01-19 13:36:05 -05:00
"""
:return: list of parts of the url that are required plus the body
"""
parts = self.all_parts
required = [p for p in parts if parts[p]["required"]] # type: ignore
2019-12-22 15:35:00 +01:00
if self.body.get("required"):
required.append("body")
return required
2019-12-17 22:15:52 +01:00
def to_python(self) -> Any:
2024-01-19 13:36:05 -05:00
"""
:return: rendered Jinja template
"""
2023-11-06 13:08:19 -05:00
try:
2024-01-19 13:36:05 -05:00
template = jinja_env.get_template(f"overrides/{self.namespace}/{self.name}")
2023-11-06 13:08:19 -05:00
except TemplateNotFound:
2024-01-19 13:36:05 -05:00
template = jinja_env.get_template("base")
2024-01-19 13:36:05 -05:00
return template.render(
api=self,
substitutions={v: k for k, v in SUBSTITUTIONS.items()},
global_query_params=GLOBAL_QUERY_PARAMS,
2019-12-17 22:15:52 +01:00
)
def read_modules() -> Any:
2024-01-19 13:36:05 -05:00
"""
checks the opensearch-api spec at
https://raw.githubusercontent.com/opensearch-project/opensearch-api-specification/main/OpenSearch.openapi.json
and parses it into one or more API modules
:return: a dict of API objects
"""
modules = {}
# Load the OpenAPI specification file
response = requests.get(
2024-01-19 13:36:05 -05:00
"https://raw.githubusercontent.com/opensearch-project/opensearch-api-"
"specification/main/OpenSearch.openapi.json"
)
data = response.json()
list_of_dicts = []
for path in data["paths"]:
2024-01-19 13:36:05 -05:00
for param in data["paths"][path]: # pylint: disable=invalid-name
if data["paths"][path][param]["x-operation-group"] == "nodes.hot_threads":
if "deprecated" in data["paths"][path][param]:
continue
2024-01-19 13:36:05 -05:00
data["paths"][path][param].update({"path": path, "method": param})
list_of_dicts.append(data["paths"][path][param])
# Update parameters in each endpoint
2024-01-19 13:36:05 -05:00
for param_dict in list_of_dicts:
if "parameters" in param_dict:
params = []
parts = []
# Iterate over the list of parameters and update them
2024-01-19 13:36:05 -05:00
for param in param_dict["parameters"]:
if "schema" in param and "$ref" in param["schema"]:
schema_path_ref = param["schema"]["$ref"].split("/")[-1]
param["schema"] = data["components"]["schemas"][schema_path_ref]
params.append(param)
else:
2024-01-19 13:36:05 -05:00
params.append(param)
# Iterate over the list of updated parameters to separate "parts" from "params"
2024-01-19 13:36:05 -05:00
params_copy = params.copy()
for param in params_copy:
if param["in"] == "path":
parts.append(param)
params.remove(param)
# Convert "params" and "parts" into the structure required for generator.
params_new = {}
parts_new = {}
2024-01-19 13:36:05 -05:00
for m in params: # pylint: disable=invalid-name
a = dict( # pylint: disable=invalid-name
type=m["schema"]["type"], description=m["description"]
) # pylint: disable=invalid-name
if "default" in m["schema"]:
2023-11-21 13:04:39 -05:00
a.update({"default": m["schema"]["default"]})
if "enum" in m["schema"]:
2023-11-21 13:04:39 -05:00
a.update({"type": "enum"})
a.update({"options": m["schema"]["enum"]})
if "deprecated" in m["schema"]:
2023-11-21 13:04:39 -05:00
a.update({"deprecated": m["schema"]["deprecated"]})
a.update(
{"deprecation_message": m["schema"]["x-deprecation-message"]}
)
2023-11-21 13:04:39 -05:00
params_new.update({m["name"]: a})
# Removing the deprecated "type"
2024-01-19 13:36:05 -05:00
if (
param_dict["x-operation-group"] != "nodes.hot_threads"
and "type" in params_new
):
params_new.pop("type")
2019-12-17 22:15:52 +01:00
if (
2024-01-19 13:36:05 -05:00
param_dict["x-operation-group"] == "cluster.health"
and "ensure_node_commissioned" in params_new
):
params_new.pop("ensure_node_commissioned")
if bool(params_new):
2024-01-19 13:36:05 -05:00
param_dict.update({"params": params_new})
2019-12-17 22:15:52 +01:00
2024-01-19 13:36:05 -05:00
param_dict.pop("parameters")
2024-01-19 13:36:05 -05:00
for n in parts: # pylint: disable=invalid-name
b = dict(type=n["schema"]["type"]) # pylint: disable=invalid-name
if "description" in n:
2023-11-21 13:04:39 -05:00
b.update({"description": n["description"]})
if "x-enum-options" in n["schema"]:
2023-11-21 13:04:39 -05:00
b.update({"options": n["schema"]["x-enum-options"]})
deprecated_new = {}
if "deprecated" in n:
2023-11-21 13:04:39 -05:00
b.update({"deprecated": n["deprecated"]})
if "x-deprecation-version" in n:
deprecated_new.update({"version": n["x-deprecation-version"]})
2019-12-17 22:15:52 +01:00
if "x-deprecation-description" in n:
deprecated_new.update(
{"description": n["x-deprecation-description"]}
)
2019-12-17 22:15:52 +01:00
2023-11-21 13:04:39 -05:00
parts_new.update({n["name"]: b})
if bool(parts_new):
2024-01-19 13:36:05 -05:00
param_dict.update({"parts": parts_new})
# Sort the input list by the value of the "x-operation-group" key
list_of_dicts = sorted(list_of_dicts, key=itemgetter("x-operation-group"))
# Group the input list by the value of the "x-operation-group" key
for key, value in groupby(list_of_dicts, key=itemgetter("x-operation-group")):
api = {}
# Extract the namespace and name from the 'x-operation-group'
if "." in key:
namespace, name = key.rsplit(".", 1)
else:
2019-12-17 22:15:52 +01:00
namespace = "__init__"
name = key
# Group the data in the current group by the "path" key
paths = []
all_paths_have_deprecation = True
for key2, value2 in groupby(value, key=itemgetter("path")):
# Extract the HTTP methods from the data in the current subgroup
methods = []
parts_final = {}
2024-01-19 13:36:05 -05:00
for z in value2: # pylint: disable=invalid-name
methods.append(z["method"].upper())
# Update 'api' dictionary
if "documentation" not in api:
documentation = {"description": z["description"]}
api.update({"documentation": documentation})
if "x-deprecation-message" in z:
x_deprecation_message = z["x-deprecation-message"]
else:
all_paths_have_deprecation = False
if "params" not in api and "params" in z:
api.update({"params": z["params"]})
if "body" not in api and "requestBody" in z:
body = {"required": False}
if "required" in z["requestBody"]:
body.update({"required": z["requestBody"]["required"]})
2024-01-19 13:36:05 -05:00
q = z["requestBody"]["content"][ # pylint: disable=invalid-name
"application/json"
]["schema"]["$ref"].split("/")[-1]
if "description" in data["components"]["schemas"][q]:
body.update(
{
"description": data["components"]["schemas"][q][
"description"
]
}
)
if "x-serialize" in data["components"]["schemas"][q]:
body.update(
{
"serialize": data["components"]["schemas"][q][
"x-serialize"
]
}
)
api.update({"body": body})
if "parts" in z:
parts_final.update(z["parts"])
if "POST" in methods or "PUT" in methods:
api.update(
{
"stability": "stable", # type: ignore
"visibility": "public", # type: ignore
"headers": {
"accept": ["application/json"],
"content_type": ["application/json"],
},
}
)
else:
api.update(
{
"stability": "stable", # type: ignore
"visibility": "public", # type: ignore
"headers": {"accept": ["application/json"]},
}
)
2019-12-17 22:15:52 +01:00
if bool(deprecated_new) and bool(parts_final):
paths.append(
{
"path": key2,
"methods": methods,
"parts": parts_final,
"deprecated": deprecated_new,
}
)
elif bool(parts_final):
paths.append({"path": key2, "methods": methods, "parts": parts_final})
else:
paths.append({"path": key2, "methods": methods})
api.update({"url": {"paths": paths}})
if all_paths_have_deprecation and x_deprecation_message is not None:
api.update({"deprecation_message": x_deprecation_message})
api = apply_patch(namespace, name, api)
2020-03-13 13:44:46 -05:00
if namespace not in modules:
modules[namespace] = Module(namespace)
2019-12-17 22:15:52 +01:00
modules[namespace].add(API(namespace, name, api))
2019-12-17 22:15:52 +01:00
return modules
def apply_patch(namespace: str, name: str, api: Any) -> Any:
2024-01-19 13:36:05 -05:00
"""
applies patches as specified in {name}.json
:param namespace: directory containing overrides
:param name: file to be prepended to ".json" containing override instructions
:param api: specific api to override
:return: modified api
"""
override_file_path = (
CODE_ROOT / "utils/templates/overrides" / namespace / f"{name}.json"
)
if os.path.exists(override_file_path):
2024-01-19 13:36:05 -05:00
with open(override_file_path, encoding="utf-8") as file:
override_json = json.load(file)
api = deepmerge.always_merger.merge(api, override_json)
return api
def dump_modules(modules: Any) -> None:
2024-01-19 13:36:05 -05:00
"""
writes out modules to disk
:param modules: a list of python modules
"""
2019-12-17 22:15:52 +01:00
for mod in modules.values():
mod.dump()
2020-05-15 09:36:47 -05:00
# Unasync all the generated async code
additional_replacements = {
# We want to rewrite to 'Transport' instead of 'SyncTransport', etc
"AsyncTransport": "Transport",
2021-08-13 15:51:50 +05:30
"AsyncOpenSearch": "OpenSearch",
2020-05-15 09:36:47 -05:00
# We don't want to rewrite this class
"AsyncSearchClient": "AsyncSearchClient",
}
rules = [
unasync.Rule(
2021-09-16 14:59:29 +05:30
fromdir="/opensearchpy/_async/client/",
todir="/opensearchpy/client/",
2020-06-24 14:25:28 -05:00
additional_replacements=additional_replacements,
2020-05-15 09:36:47 -05:00
),
]
filepaths = []
2021-09-16 14:59:29 +05:30
for root, _, filenames in os.walk(CODE_ROOT / "opensearchpy/_async"):
2020-05-15 09:36:47 -05:00
for filename in filenames:
2023-11-06 13:08:19 -05:00
if filename.rpartition(".")[-1] in ("py",) and not filename.startswith(
"utils.py"
):
2020-05-15 09:36:47 -05:00
filepaths.append(os.path.join(root, filename))
unasync.unasync_files(filepaths, rules)
2021-09-16 14:59:29 +05:30
blacken(CODE_ROOT / "opensearchpy")
2020-05-15 09:36:47 -05:00
2019-12-17 22:15:52 +01:00
if __name__ == "__main__":
dump_modules(read_modules())