2d03dc773b
* Alert Plugin Signed-off-by: Arnav Das <arnav.das88@gmail.com> * lint file header license Signed-off-by: Arnav Das <arnav.das88@gmail.com> * Tests For Alerting[Monitors] Signed-off-by: Arnav Das <arnav.das88@gmail.com> * Tests For Alerting[Destinations] Signed-off-by: Arnav Das <arnav.das88@gmail.com> * dynamic lookup 90#issuecomment-1003396742 Signed-off-by: Arnav Das <arnav.das88@gmail.com> * Alerting Async Signed-off-by: Arnav Das <arnav.das88@gmail.com> * Lint Changes Signed-off-by: Arnav Das <arnav.das88@gmail.com> * alerting plugins integration tests Signed-off-by: Arnav Das <arnav.das88@gmail.com> * Integ UnitTests Skip on OPENSEARCH_VERSION >= 2.0.0 Signed-off-by: Arnav Das <arnav.das88@gmail.com> * Unit Test Skip Complete Signed-off-by: Arnav Das <arnav.das88@gmail.com> * License Headers fixed Signed-off-by: Arnav Das <arnav.das88@gmail.com> * test_urllib3_connection timeout Signed-off-by: Arnav Das <arnav.das88@gmail.com> * lint Signed-off-by: Arnav Das <arnav.das88@gmail.com> * Fix licenses in all files Signed-off-by: Harsha Vamsi Kalluri <harshavamsi096@gmail.com> * Remove elastic search licenses on new files Signed-off-by: Harsha Vamsi Kalluri <harshavamsi096@gmail.com> * Fix formatting Signed-off-by: Harsha Vamsi Kalluri <harshavamsi096@gmail.com> Signed-off-by: Arnav Das <arnav.das88@gmail.com> Signed-off-by: Harsha Vamsi Kalluri <harshavamsi096@gmail.com> Co-authored-by: Harsha Vamsi Kalluri <harshavamsi096@gmail.com>
437 lines
13 KiB
Python
437 lines
13 KiB
Python
#!/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.
|
|
#
|
|
# 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
|
|
# 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.
|
|
|
|
|
|
import contextlib
|
|
import io
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import zipfile
|
|
from functools import lru_cache
|
|
from itertools import chain
|
|
from pathlib import Path
|
|
|
|
import black
|
|
import unasync
|
|
import urllib3
|
|
from click.testing import CliRunner
|
|
from jinja2 import Environment, FileSystemLoader, TemplateNotFound, select_autoescape
|
|
|
|
http = urllib3.PoolManager()
|
|
|
|
# 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_"}
|
|
# api path(s)
|
|
BRANCH_NAME = "7.x"
|
|
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]]]",
|
|
}
|
|
|
|
jinja_env = Environment(
|
|
autoescape=select_autoescape(["html", "xml"]),
|
|
loader=FileSystemLoader([CODE_ROOT / "utils" / "templates"]),
|
|
trim_blocks=True,
|
|
lstrip_blocks=True,
|
|
)
|
|
|
|
|
|
def blacken(filename):
|
|
runner = CliRunner()
|
|
result = runner.invoke(black.main, [str(filename)])
|
|
assert result.exit_code == 0, result.output
|
|
|
|
|
|
@lru_cache()
|
|
def is_valid_url(url):
|
|
return 200 <= http.request("HEAD", url).status < 400
|
|
|
|
|
|
class Module:
|
|
def __init__(self, namespace, is_pyi=False):
|
|
self.namespace = namespace
|
|
self.is_pyi = is_pyi
|
|
self._apis = []
|
|
self.parse_orig()
|
|
|
|
if not is_pyi:
|
|
self.pyi = Module(namespace, is_pyi=True)
|
|
self.pyi.orders = self.orders[:]
|
|
|
|
def add(self, api):
|
|
self._apis.append(api)
|
|
|
|
def parse_orig(self):
|
|
self.orders = []
|
|
self.header = "class C:"
|
|
if os.path.exists(self.filepath):
|
|
with open(self.filepath) as f:
|
|
content = f.read()
|
|
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)
|
|
if line.startswith("class"):
|
|
break
|
|
self.header = "\n".join(header_lines)
|
|
self.orders = re.findall(
|
|
r"\n (?:async )?def ([a-z_]+)\(", content, re.MULTILINE
|
|
)
|
|
|
|
def _position(self, api):
|
|
try:
|
|
return self.orders.index(api.name)
|
|
except ValueError:
|
|
return len(self.orders)
|
|
|
|
def sort(self):
|
|
self._apis.sort(key=self._position)
|
|
|
|
def dump(self):
|
|
self.sort()
|
|
with open(self.filepath, "w") as f:
|
|
f.write(self.header)
|
|
for api in self._apis:
|
|
f.write(api.to_python())
|
|
|
|
if not self.is_pyi:
|
|
self.pyi.dump()
|
|
|
|
@property
|
|
def filepath(self):
|
|
return (
|
|
CODE_ROOT
|
|
/ f"opensearchpy/_async/client/{self.namespace}.py{'i' if self.is_pyi else ''}"
|
|
)
|
|
|
|
|
|
class API:
|
|
def __init__(self, namespace, name, definition, is_pyi=False):
|
|
self.namespace = namespace
|
|
self.name = name
|
|
self.is_pyi = is_pyi
|
|
|
|
# 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")
|
|
|
|
if isinstance(definition["documentation"], str):
|
|
self.doc_url = definition["documentation"]
|
|
else:
|
|
# set as attribute so it may be overridden by Module.add
|
|
self.description = (
|
|
definition["documentation"].get("description", "").strip()
|
|
)
|
|
self.doc_url = definition["documentation"].get("url", "")
|
|
|
|
# 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://")
|
|
|
|
# Try setting doc refs like 'current' and 'master' to our branches ref.
|
|
if BRANCH_NAME is not None:
|
|
revised_url = re.sub(
|
|
"/opensearchpy/reference/[^/]+/",
|
|
f"/opensearchpy/reference/{BRANCH_NAME}/",
|
|
self.doc_url,
|
|
)
|
|
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):
|
|
parts = {}
|
|
for url in self._def["url"]["paths"]:
|
|
parts.update(url.get("parts", {}))
|
|
|
|
for p in parts:
|
|
parts[p]["required"] = all(
|
|
p in url.get("parts", {}) for url in self._def["url"]["paths"]
|
|
)
|
|
parts[p]["type"] = "Any"
|
|
|
|
# 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
|
|
|
|
for k, sub in SUBSTITUTIONS.items():
|
|
if k in parts:
|
|
parts[sub] = parts.pop(k)
|
|
|
|
dynamic, components = self.url_parts
|
|
|
|
def ind(item):
|
|
try:
|
|
return components.index(item[0])
|
|
except ValueError:
|
|
return len(components)
|
|
|
|
parts = dict(sorted(parts.items(), key=ind))
|
|
return parts
|
|
|
|
@property
|
|
def params(self):
|
|
parts = self.all_parts
|
|
params = self._def.get("params", {})
|
|
return chain(
|
|
((p, parts[p]) for p in parts if parts[p]["required"]),
|
|
(("body", self.body),) if self.body else (),
|
|
(
|
|
(p, parts[p])
|
|
for p in parts
|
|
if not parts[p]["required"] and p not in params
|
|
),
|
|
sorted(params.items(), key=lambda x: (x[0] not in parts, x[0])),
|
|
)
|
|
|
|
@property
|
|
def body(self):
|
|
b = self._def.get("body", {})
|
|
if b:
|
|
b.setdefault("required", False)
|
|
return b
|
|
|
|
@property
|
|
def query_params(self):
|
|
return (
|
|
k
|
|
for k in sorted(self._def.get("params", {}).keys())
|
|
if k not in self.all_parts
|
|
)
|
|
|
|
@property
|
|
def all_func_params(self):
|
|
"""Parameters that will be in the '@query_params' decorator list
|
|
and parameters that will be in the function signature.
|
|
This doesn't include
|
|
"""
|
|
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
|
|
|
|
@property
|
|
def path(self):
|
|
return max(
|
|
(path for path in self._def["url"]["paths"]),
|
|
key=lambda p: len(re.findall(r"\{([^}]+)\}", p["path"])),
|
|
)
|
|
|
|
@property
|
|
def method(self):
|
|
# To adhere to the HTTP RFC we shouldn't send
|
|
# bodies in GET requests.
|
|
default_method = self.path["methods"][0]
|
|
if self.body and default_method == "GET" and "POST" in self.path["methods"]:
|
|
return "POST"
|
|
return default_method
|
|
|
|
@property
|
|
def url_parts(self):
|
|
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):
|
|
parts = self.all_parts
|
|
required = [p for p in parts if parts[p]["required"]]
|
|
if self.body.get("required"):
|
|
required.append("body")
|
|
return required
|
|
|
|
def to_python(self):
|
|
if self.is_pyi:
|
|
t = jinja_env.get_template("base_pyi")
|
|
else:
|
|
try:
|
|
t = jinja_env.get_template(f"overrides/{self.namespace}/{self.name}")
|
|
except TemplateNotFound:
|
|
t = jinja_env.get_template("base")
|
|
|
|
return t.render(
|
|
api=self,
|
|
substitutions={v: k for k, v in SUBSTITUTIONS.items()},
|
|
global_query_params=GLOBAL_QUERY_PARAMS,
|
|
)
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def download_artifact(version):
|
|
# Download the list of all artifacts for a version
|
|
# and find the latest build URL for 'rest-resources-zip-*.zip'
|
|
resp = http.request(
|
|
"GET", f"https://artifacts-api.elastic.co/v1/versions/{version}"
|
|
)
|
|
packages = json.loads(resp.data)["version"]["builds"][0]["projects"][
|
|
"opensearchpy"
|
|
]["packages"]
|
|
for package in packages:
|
|
if re.match(r"^rest-resources-zip-.*\.zip$", package):
|
|
zip_url = packages[package]["url"]
|
|
break
|
|
else:
|
|
raise RuntimeError(
|
|
"Could not find the package 'rest-resources-zip-*.zip' in build"
|
|
)
|
|
|
|
# Download the .jar file and unzip only the API
|
|
# .json files into a temporary directory
|
|
resp = http.request("GET", zip_url)
|
|
|
|
tmp = Path(tempfile.mkdtemp())
|
|
zip = zipfile.ZipFile(io.BytesIO(resp.data))
|
|
for name in zip.namelist():
|
|
if not name.endswith(".json") or name == "schema.json":
|
|
continue
|
|
with (tmp / name.replace("rest-api-spec/api/", "")).open("wb") as f:
|
|
f.write(zip.read(name))
|
|
|
|
yield tmp
|
|
shutil.rmtree(tmp)
|
|
|
|
|
|
def read_modules(version):
|
|
modules = {}
|
|
|
|
with download_artifact(version) as path:
|
|
for f in sorted(os.listdir(path)):
|
|
name, ext = f.rsplit(".", 1)
|
|
|
|
if ext != "json" or name == "_common":
|
|
continue
|
|
|
|
with open(path / f) as api_def:
|
|
api = json.load(api_def)[name]
|
|
|
|
namespace = "__init__"
|
|
if "." in name:
|
|
namespace, name = name.rsplit(".", 1)
|
|
|
|
# The data_frame API has been changed to transform.
|
|
if namespace == "data_frame_transform_deprecated":
|
|
continue
|
|
|
|
if namespace not in modules:
|
|
modules[namespace] = Module(namespace)
|
|
|
|
modules[namespace].add(API(namespace, name, api))
|
|
modules[namespace].pyi.add(API(namespace, name, api, is_pyi=True))
|
|
|
|
return modules
|
|
|
|
|
|
def dump_modules(modules):
|
|
for mod in modules.values():
|
|
mod.dump()
|
|
|
|
# Unasync all the generated async code
|
|
additional_replacements = {
|
|
# We want to rewrite to 'Transport' instead of 'SyncTransport', etc
|
|
"AsyncTransport": "Transport",
|
|
"AsyncOpenSearch": "OpenSearch",
|
|
# We don't want to rewrite this class
|
|
"AsyncSearchClient": "AsyncSearchClient",
|
|
}
|
|
rules = [
|
|
unasync.Rule(
|
|
fromdir="/opensearchpy/_async/client/",
|
|
todir="/opensearchpy/client/",
|
|
additional_replacements=additional_replacements,
|
|
),
|
|
]
|
|
|
|
filepaths = []
|
|
for root, _, filenames in os.walk(CODE_ROOT / "opensearchpy/_async"):
|
|
for filename in filenames:
|
|
if filename.rpartition(".")[-1] in (
|
|
"py",
|
|
"pyi",
|
|
) and not filename.startswith("utils.py"):
|
|
filepaths.append(os.path.join(root, filename))
|
|
|
|
unasync.unasync_files(filepaths, rules)
|
|
blacken(CODE_ROOT / "opensearchpy")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
version = sys.argv[1]
|
|
dump_modules(read_modules(version))
|