2020-09-15 00:24:01 +02:00
|
|
|
# Zulip's OpenAPI-based API documentation system is documented at
|
|
|
|
# https://zulip.readthedocs.io/en/latest/documentation/api.html
|
|
|
|
#
|
|
|
|
# This file defines the special Markdown extension that is used to
|
|
|
|
# render the code examples, example responses, etc. that appear in
|
|
|
|
# Zulip's public API documentation.
|
|
|
|
|
2018-01-26 22:08:42 +01:00
|
|
|
import inspect
|
2020-06-11 00:54:34 +02:00
|
|
|
import json
|
|
|
|
import re
|
2020-11-04 02:49:09 +01:00
|
|
|
import shlex
|
2020-12-11 05:39:23 +01:00
|
|
|
from textwrap import dedent
|
2021-09-30 00:10:12 +02:00
|
|
|
from typing import Any, Dict, List, Mapping, Match, Optional, Pattern
|
2018-01-26 22:08:42 +01:00
|
|
|
|
2020-06-11 00:54:34 +02:00
|
|
|
import markdown
|
2019-10-21 12:43:00 +02:00
|
|
|
from django.conf import settings
|
2018-01-26 22:08:42 +01:00
|
|
|
from markdown.extensions import Extension
|
|
|
|
from markdown.preprocessors import Preprocessor
|
2023-10-12 19:43:45 +02:00
|
|
|
from typing_extensions import override
|
2018-01-26 22:08:42 +01:00
|
|
|
|
2019-08-04 18:14:48 +02:00
|
|
|
import zerver.openapi.python_examples
|
2024-05-20 22:09:35 +02:00
|
|
|
from zerver.lib.markdown.priorities import PREPROCESSOR_PRIORITIES
|
2021-05-22 13:11:23 +02:00
|
|
|
from zerver.openapi.openapi import (
|
2024-02-01 06:26:58 +01:00
|
|
|
NO_EXAMPLE,
|
|
|
|
Parameter,
|
2021-06-24 15:59:47 +02:00
|
|
|
check_additional_imports,
|
2021-06-11 20:07:45 +02:00
|
|
|
check_requires_administrator,
|
2021-06-07 22:14:34 +02:00
|
|
|
generate_openapi_fixture,
|
2021-06-21 12:53:05 +02:00
|
|
|
get_curl_include_exclude,
|
2021-05-22 13:11:23 +02:00
|
|
|
get_openapi_description,
|
2024-02-01 06:26:58 +01:00
|
|
|
get_openapi_parameters,
|
2021-05-22 13:11:23 +02:00
|
|
|
get_openapi_summary,
|
2021-06-21 22:22:27 +02:00
|
|
|
get_parameters_description,
|
2021-06-21 21:56:18 +02:00
|
|
|
get_responses_description,
|
2021-05-22 13:11:23 +02:00
|
|
|
openapi_spec,
|
|
|
|
)
|
2018-01-26 22:08:42 +01:00
|
|
|
|
2022-01-13 17:52:46 +01:00
|
|
|
API_ENDPOINT_NAME = r"/[a-z_\-/-{}]+:[a-z]+"
|
2021-09-30 00:10:12 +02:00
|
|
|
API_LANGUAGE = r"\w+"
|
|
|
|
API_KEY_TYPE = r"fixture|example"
|
2020-05-06 00:44:08 +02:00
|
|
|
MACRO_REGEXP = re.compile(
|
2021-09-30 00:10:12 +02:00
|
|
|
rf"""
|
|
|
|
{{
|
|
|
|
generate_code_example
|
|
|
|
(?: \( \s* ({API_LANGUAGE}) \s* \) )?
|
|
|
|
\|
|
|
|
|
\s* ({API_ENDPOINT_NAME}) \s*
|
|
|
|
\|
|
|
|
|
\s* ({API_KEY_TYPE}) \s*
|
|
|
|
}}
|
|
|
|
""",
|
|
|
|
re.VERBOSE,
|
|
|
|
)
|
|
|
|
PYTHON_EXAMPLE_REGEX = re.compile(r"\# \{code_example\|\s*(start|end)\s*\}")
|
|
|
|
JS_EXAMPLE_REGEX = re.compile(r"\/\/ \{code_example\|\s*(start|end)\s*\}")
|
2022-07-21 00:01:06 +02:00
|
|
|
MACRO_REGEXP_HEADER = re.compile(rf"{{generate_api_header\(\s*({API_ENDPOINT_NAME})\s*\)}}")
|
2021-09-30 00:10:12 +02:00
|
|
|
MACRO_REGEXP_RESPONSE_DESC = re.compile(
|
|
|
|
rf"{{generate_response_description\(\s*({API_ENDPOINT_NAME})\s*\)}}"
|
|
|
|
)
|
|
|
|
MACRO_REGEXP_PARAMETER_DESC = re.compile(
|
|
|
|
rf"{{generate_parameter_description\(\s*({API_ENDPOINT_NAME})\s*\)}}"
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2018-01-26 22:08:42 +01:00
|
|
|
|
2018-02-16 04:09:21 +01:00
|
|
|
PYTHON_CLIENT_CONFIG = """
|
2018-01-26 22:08:42 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
import zulip
|
|
|
|
|
2018-10-16 21:23:23 +02:00
|
|
|
# Pass the path to your zuliprc file here.
|
|
|
|
client = zulip.Client(config_file="~/zuliprc")
|
2018-01-26 22:08:42 +01:00
|
|
|
|
|
|
|
"""
|
|
|
|
|
2018-01-31 05:34:53 +01:00
|
|
|
PYTHON_CLIENT_ADMIN_CONFIG = """
|
|
|
|
#!/usr/bin/env python
|
|
|
|
|
|
|
|
import zulip
|
|
|
|
|
2018-10-16 21:23:23 +02:00
|
|
|
# The user for this zuliprc file must be an organization administrator
|
2018-01-31 05:34:53 +01:00
|
|
|
client = zulip.Client(config_file="~/zuliprc-admin")
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
2020-05-17 12:04:53 +02:00
|
|
|
JS_CLIENT_CONFIG = """
|
2020-12-11 05:39:23 +01:00
|
|
|
const zulipInit = require("zulip-js");
|
2020-05-17 12:04:53 +02:00
|
|
|
|
|
|
|
// Pass the path to your zuliprc file here.
|
2020-12-11 05:39:23 +01:00
|
|
|
const config = { zuliprc: "zuliprc" };
|
2020-05-17 12:04:53 +02:00
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
JS_CLIENT_ADMIN_CONFIG = """
|
2020-12-11 05:39:23 +01:00
|
|
|
const zulipInit = require("zulip-js");
|
2020-05-17 12:04:53 +02:00
|
|
|
|
|
|
|
// The user for this zuliprc file must be an organization administrator.
|
2020-12-11 05:39:23 +01:00
|
|
|
const config = { zuliprc: "zuliprc-admin" };
|
2020-05-17 12:04:53 +02:00
|
|
|
|
|
|
|
"""
|
|
|
|
|
2019-07-29 15:46:48 +02:00
|
|
|
DEFAULT_AUTH_EMAIL = "BOT_EMAIL_ADDRESS"
|
|
|
|
DEFAULT_AUTH_API_KEY = "BOT_API_KEY"
|
|
|
|
DEFAULT_EXAMPLE = {
|
|
|
|
"integer": 1,
|
|
|
|
"string": "demo",
|
|
|
|
"boolean": False,
|
|
|
|
}
|
2021-06-11 20:07:45 +02:00
|
|
|
ADMIN_CONFIG_LANGUAGES = ["python", "javascript"]
|
2019-07-29 15:46:48 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def extract_code_example(
|
|
|
|
source: List[str], snippet: List[Any], example_regex: Pattern[str]
|
|
|
|
) -> List[Any]:
|
2018-02-16 04:09:21 +01:00
|
|
|
start = -1
|
|
|
|
end = -1
|
|
|
|
for line in source:
|
2020-04-21 20:57:11 +02:00
|
|
|
match = example_regex.search(line)
|
2018-02-16 04:09:21 +01:00
|
|
|
if match:
|
2021-02-12 08:20:45 +01:00
|
|
|
if match.group(1) == "start":
|
2018-02-16 04:09:21 +01:00
|
|
|
start = source.index(line)
|
2021-02-12 08:20:45 +01:00
|
|
|
elif match.group(1) == "end":
|
2018-02-16 04:09:21 +01:00
|
|
|
end = source.index(line)
|
|
|
|
break
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
if start == -1 and end == -1:
|
2018-02-16 04:09:21 +01:00
|
|
|
return snippet
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
snippet.append(source[start + 1 : end])
|
|
|
|
source = source[end + 1 :]
|
2020-04-21 20:57:11 +02:00
|
|
|
return extract_code_example(source, snippet, example_regex)
|
2018-02-16 04:09:21 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def render_python_code_example(
|
|
|
|
function: str, admin_config: bool = False, **kwargs: Any
|
|
|
|
) -> List[str]:
|
2021-06-23 13:53:52 +02:00
|
|
|
if function not in zerver.openapi.python_examples.TEST_FUNCTIONS:
|
|
|
|
return []
|
2019-08-04 18:14:48 +02:00
|
|
|
method = zerver.openapi.python_examples.TEST_FUNCTIONS[function]
|
2018-02-16 04:09:21 +01:00
|
|
|
function_source_lines = inspect.getsourcelines(method)[0]
|
|
|
|
|
|
|
|
if admin_config:
|
2021-06-24 15:59:47 +02:00
|
|
|
config_string = PYTHON_CLIENT_ADMIN_CONFIG
|
2018-02-16 04:09:21 +01:00
|
|
|
else:
|
2021-06-24 15:59:47 +02:00
|
|
|
config_string = PYTHON_CLIENT_CONFIG
|
|
|
|
|
|
|
|
endpoint, endpoint_method = function.split(":")
|
|
|
|
extra_imports = check_additional_imports(endpoint, endpoint_method)
|
|
|
|
if extra_imports:
|
2023-01-26 00:12:09 +01:00
|
|
|
extra_imports = sorted([*extra_imports, "zulip"])
|
2021-06-24 15:59:47 +02:00
|
|
|
extra_imports = [f"import {each_import}" for each_import in extra_imports]
|
|
|
|
config_string = config_string.replace("import zulip", "\n".join(extra_imports))
|
|
|
|
|
|
|
|
config = config_string.splitlines()
|
2018-02-16 04:09:21 +01:00
|
|
|
|
2020-05-20 15:43:59 +02:00
|
|
|
snippets = extract_code_example(function_source_lines, [], PYTHON_EXAMPLE_REGEX)
|
2018-02-16 04:09:21 +01:00
|
|
|
|
2023-07-31 22:52:35 +02:00
|
|
|
return [
|
|
|
|
"{tab|python}\n",
|
|
|
|
"```python",
|
|
|
|
*config,
|
|
|
|
# Remove one level of indentation and strip newlines
|
|
|
|
*(line[4:].rstrip() for snippet in snippets for line in snippet),
|
|
|
|
"print(result)",
|
|
|
|
"\n",
|
|
|
|
"```",
|
|
|
|
]
|
2018-02-16 04:09:21 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def render_javascript_code_example(
|
|
|
|
function: str, admin_config: bool = False, **kwargs: Any
|
|
|
|
) -> List[str]:
|
2022-02-15 23:45:41 +01:00
|
|
|
pattern = rf'^add_example\(\s*"[^"]*",\s*{re.escape(json.dumps(function))},\s*\d+,\s*async \(client, console\) => \{{\n(.*?)^(?:\}}| *\}},\n)\);$'
|
2021-02-12 08:20:45 +01:00
|
|
|
with open("zerver/openapi/javascript_examples.js") as f:
|
2020-12-11 05:39:23 +01:00
|
|
|
m = re.search(pattern, f.read(), re.M | re.S)
|
2021-06-02 18:36:25 +02:00
|
|
|
if m is None:
|
|
|
|
return []
|
2020-12-11 05:39:23 +01:00
|
|
|
function_source_lines = dedent(m.group(1)).splitlines()
|
2020-05-19 18:40:35 +02:00
|
|
|
|
|
|
|
snippets = extract_code_example(function_source_lines, [], JS_EXAMPLE_REGEX)
|
|
|
|
|
2020-05-17 12:04:53 +02:00
|
|
|
if admin_config:
|
|
|
|
config = JS_CLIENT_ADMIN_CONFIG.splitlines()
|
|
|
|
else:
|
|
|
|
config = JS_CLIENT_CONFIG.splitlines()
|
|
|
|
|
2021-06-02 18:36:25 +02:00
|
|
|
code_example = [
|
|
|
|
"{tab|js}\n",
|
|
|
|
"More examples and documentation can be found [here](https://github.com/zulip/zulip-js).",
|
|
|
|
]
|
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
code_example.append("```js")
|
2020-05-17 12:04:53 +02:00
|
|
|
code_example.extend(config)
|
2020-12-11 05:39:23 +01:00
|
|
|
code_example.append("(async () => {")
|
|
|
|
code_example.append(" const client = await zulipInit(config);")
|
2020-05-19 18:40:35 +02:00
|
|
|
for snippet in snippets:
|
2020-12-11 05:39:23 +01:00
|
|
|
code_example.append("")
|
2023-07-31 22:52:35 +02:00
|
|
|
# Strip newlines
|
|
|
|
code_example.extend(" " + line.rstrip() for line in snippet)
|
2020-12-11 05:39:23 +01:00
|
|
|
code_example.append("})();")
|
2020-05-19 18:40:35 +02:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
code_example.append("```")
|
2020-05-19 18:40:35 +02:00
|
|
|
|
2020-05-17 12:04:53 +02:00
|
|
|
return code_example
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def curl_method_arguments(endpoint: str, method: str, api_url: str) -> List[str]:
|
2019-08-07 10:55:41 +02:00
|
|
|
# We also include the -sS verbosity arguments here.
|
2019-07-29 15:46:48 +02:00
|
|
|
method = method.upper()
|
2020-06-09 00:25:09 +02:00
|
|
|
url = f"{api_url}/v1{endpoint}"
|
2019-07-29 15:46:48 +02:00
|
|
|
valid_methods = ["GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS"]
|
2019-08-07 10:55:41 +02:00
|
|
|
if method == "GET":
|
2019-07-29 15:46:48 +02:00
|
|
|
# Then we need to make sure that each -d option translates to becoming
|
|
|
|
# a GET parameter (in the URL) and not a POST parameter (in the body).
|
|
|
|
# TODO: remove the -X part by updating the linting rule. It's redundant.
|
2019-08-07 10:55:41 +02:00
|
|
|
return ["-sSX", "GET", "-G", url]
|
2019-07-29 15:46:48 +02:00
|
|
|
elif method in valid_methods:
|
2019-08-07 10:55:41 +02:00
|
|
|
return ["-sSX", method, url]
|
2019-07-29 15:46:48 +02:00
|
|
|
else:
|
2020-06-10 06:40:53 +02:00
|
|
|
msg = f"The request method {method} is not one of {valid_methods}"
|
2019-07-29 15:46:48 +02:00
|
|
|
raise ValueError(msg)
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def get_openapi_param_example_value_as_string(
|
2024-02-01 06:26:58 +01:00
|
|
|
endpoint: str, method: str, parameter: Parameter, curl_argument: bool = False
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> str:
|
2024-02-01 06:26:58 +01:00
|
|
|
if "type" in parameter.value_schema:
|
|
|
|
param_type = parameter.value_schema["type"]
|
2020-01-28 07:28:22 +01:00
|
|
|
else:
|
|
|
|
# Hack: Ideally, we'd extract a common function for handling
|
|
|
|
# oneOf values in types and do something with the resulting
|
|
|
|
# union type. But for this logic's purpose, it's good enough
|
|
|
|
# to just check the first parameter.
|
2024-02-01 06:26:58 +01:00
|
|
|
param_type = parameter.value_schema["oneOf"][0]["type"]
|
2021-04-07 18:47:48 +02:00
|
|
|
|
2019-10-03 15:21:33 +02:00
|
|
|
if param_type in ["object", "array"]:
|
2024-02-01 06:26:58 +01:00
|
|
|
if parameter.example is NO_EXAMPLE:
|
2020-06-13 08:57:35 +02:00
|
|
|
msg = f"""All array and object type request parameters must have
|
|
|
|
concrete examples. The openAPI documentation for {endpoint}/{method} is missing an example
|
2024-02-01 06:26:58 +01:00
|
|
|
value for the {parameter.name} parameter. Without this we cannot automatically generate a
|
2020-06-13 08:57:35 +02:00
|
|
|
cURL example."""
|
2019-10-03 15:21:33 +02:00
|
|
|
raise ValueError(msg)
|
2024-02-01 06:26:58 +01:00
|
|
|
ordered_ex_val_str = json.dumps(parameter.example, sort_keys=True)
|
2020-06-27 19:23:50 +02:00
|
|
|
# We currently don't have any non-JSON encoded arrays.
|
2024-02-01 06:26:58 +01:00
|
|
|
assert parameter.json_encoded
|
2019-10-03 15:21:33 +02:00
|
|
|
if curl_argument:
|
2024-02-01 06:26:58 +01:00
|
|
|
return " --data-urlencode " + shlex.quote(f"{parameter.name}={ordered_ex_val_str}")
|
2019-10-03 15:21:33 +02:00
|
|
|
return ordered_ex_val_str # nocoverage
|
|
|
|
else:
|
2024-02-01 06:26:58 +01:00
|
|
|
if parameter.example is NO_EXAMPLE:
|
|
|
|
example_value = DEFAULT_EXAMPLE[param_type]
|
|
|
|
else:
|
|
|
|
example_value = parameter.example
|
|
|
|
|
|
|
|
# Booleans are effectively JSON-encoded, in that we pass
|
|
|
|
# true/false, not the Python str(True) = "True"
|
|
|
|
if parameter.json_encoded or isinstance(example_value, (bool, float, int)):
|
2019-10-18 08:47:27 +02:00
|
|
|
example_value = json.dumps(example_value)
|
2024-02-01 06:26:58 +01:00
|
|
|
else:
|
|
|
|
assert isinstance(example_value, str)
|
|
|
|
|
2019-10-03 15:21:33 +02:00
|
|
|
if curl_argument:
|
2024-02-01 06:26:58 +01:00
|
|
|
return " --data-urlencode " + shlex.quote(f"{parameter.name}={example_value}")
|
2019-10-03 15:21:33 +02:00
|
|
|
return example_value
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def generate_curl_example(
|
|
|
|
endpoint: str,
|
|
|
|
method: str,
|
|
|
|
api_url: str,
|
|
|
|
auth_email: str = DEFAULT_AUTH_EMAIL,
|
|
|
|
auth_api_key: str = DEFAULT_AUTH_API_KEY,
|
|
|
|
exclude: Optional[List[str]] = None,
|
|
|
|
include: Optional[List[str]] = None,
|
|
|
|
) -> List[str]:
|
2019-07-29 15:46:48 +02:00
|
|
|
lines = ["```curl"]
|
2019-12-04 12:20:51 +01:00
|
|
|
operation = endpoint + ":" + method.lower()
|
2021-02-12 08:20:45 +01:00
|
|
|
operation_entry = openapi_spec.openapi()["paths"][endpoint][method.lower()]
|
|
|
|
global_security = openapi_spec.openapi()["security"]
|
2019-12-04 12:27:15 +01:00
|
|
|
|
2024-02-01 06:26:58 +01:00
|
|
|
parameters = get_openapi_parameters(endpoint, method)
|
2019-12-04 12:20:51 +01:00
|
|
|
operation_request_body = operation_entry.get("requestBody", None)
|
2019-12-04 12:27:15 +01:00
|
|
|
operation_security = operation_entry.get("security", None)
|
2019-07-29 15:46:48 +02:00
|
|
|
|
2019-10-21 12:43:00 +02:00
|
|
|
if settings.RUNNING_OPENAPI_CURL_TEST: # nocoverage
|
2019-11-18 15:48:49 +01:00
|
|
|
from zerver.openapi.curl_param_value_generators import patch_openapi_example_values
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2024-02-01 06:26:58 +01:00
|
|
|
parameters, operation_request_body = patch_openapi_example_values(
|
|
|
|
operation, parameters, operation_request_body
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2019-10-21 12:43:00 +02:00
|
|
|
|
2019-10-03 15:59:28 +02:00
|
|
|
format_dict = {}
|
2024-02-01 06:26:58 +01:00
|
|
|
for parameter in parameters:
|
|
|
|
if parameter.kind != "path":
|
2019-10-03 15:59:28 +02:00
|
|
|
continue
|
2024-02-01 06:26:58 +01:00
|
|
|
example_value = get_openapi_param_example_value_as_string(endpoint, method, parameter)
|
|
|
|
format_dict[parameter.name] = example_value
|
2019-10-03 15:59:28 +02:00
|
|
|
example_endpoint = endpoint.format_map(format_dict)
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
curl_first_line_parts = ["curl", *curl_method_arguments(example_endpoint, method, api_url)]
|
2022-04-27 02:10:28 +02:00
|
|
|
lines.append(shlex.join(curl_first_line_parts))
|
2019-07-29 15:46:48 +02:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
insecure_operations = ["/dev_fetch_api_key:post", "/fetch_api_key:post"]
|
2019-12-04 12:27:15 +01:00
|
|
|
if operation_security is None:
|
2021-02-12 08:20:45 +01:00
|
|
|
if global_security == [{"basicAuth": []}]:
|
2019-12-04 12:27:15 +01:00
|
|
|
authentication_required = True
|
|
|
|
else:
|
2021-02-12 08:19:30 +01:00
|
|
|
raise AssertionError(
|
2023-01-03 02:16:53 +01:00
|
|
|
"Unhandled global securityScheme. Please update the code to handle this scheme."
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2019-12-04 12:27:15 +01:00
|
|
|
elif operation_security == []:
|
|
|
|
if operation in insecure_operations:
|
|
|
|
authentication_required = False
|
|
|
|
else:
|
2021-02-12 08:19:30 +01:00
|
|
|
raise AssertionError(
|
2023-01-03 02:16:53 +01:00
|
|
|
"Unknown operation without a securityScheme. Please update insecure_operations."
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2019-12-04 12:27:15 +01:00
|
|
|
else:
|
2021-02-12 08:19:30 +01:00
|
|
|
raise AssertionError(
|
|
|
|
"Unhandled securityScheme. Please update the code to handle this scheme."
|
|
|
|
)
|
2019-12-04 12:27:15 +01:00
|
|
|
|
2019-07-29 15:46:48 +02:00
|
|
|
if authentication_required:
|
2020-11-04 02:49:09 +01:00
|
|
|
lines.append(" -u " + shlex.quote(f"{auth_email}:{auth_api_key}"))
|
2019-07-29 15:46:48 +02:00
|
|
|
|
2024-02-01 06:26:58 +01:00
|
|
|
for parameter in parameters:
|
|
|
|
if parameter.kind == "path":
|
2019-10-09 13:01:07 +02:00
|
|
|
continue
|
|
|
|
|
2024-02-01 06:26:58 +01:00
|
|
|
if include is not None and parameter.name not in include:
|
2019-10-03 15:02:51 +02:00
|
|
|
continue
|
2019-10-09 13:01:07 +02:00
|
|
|
|
2024-02-01 06:26:58 +01:00
|
|
|
if exclude is not None and parameter.name in exclude:
|
2019-10-09 13:01:07 +02:00
|
|
|
continue
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
example_value = get_openapi_param_example_value_as_string(
|
2024-02-01 06:26:58 +01:00
|
|
|
endpoint, method, parameter, curl_argument=True
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2019-10-03 15:21:33 +02:00
|
|
|
lines.append(example_value)
|
2019-07-29 15:46:48 +02:00
|
|
|
|
2024-02-01 06:07:29 +01:00
|
|
|
if "requestBody" in operation_entry and "multipart/form-data" in (
|
|
|
|
content := operation_entry["requestBody"]["content"]
|
|
|
|
):
|
|
|
|
properties = content["multipart/form-data"]["schema"]["properties"]
|
2019-10-16 13:06:31 +02:00
|
|
|
for key, property in properties.items():
|
2021-02-12 08:20:45 +01:00
|
|
|
lines.append(" -F " + shlex.quote("{}=@{}".format(key, property["example"])))
|
2019-10-16 13:06:31 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
for i in range(1, len(lines) - 1):
|
2019-07-29 15:46:48 +02:00
|
|
|
lines[i] = lines[i] + " \\"
|
|
|
|
|
|
|
|
lines.append("```")
|
|
|
|
|
|
|
|
return lines
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def render_curl_example(
|
|
|
|
function: str,
|
|
|
|
api_url: str,
|
2021-09-30 00:10:12 +02:00
|
|
|
admin_config: bool = False,
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> List[str]:
|
2021-05-08 02:36:30 +02:00
|
|
|
"""A simple wrapper around generate_curl_example."""
|
2019-07-29 15:46:48 +02:00
|
|
|
parts = function.split(":")
|
|
|
|
endpoint = parts[0]
|
|
|
|
method = parts[1]
|
2020-09-02 08:14:51 +02:00
|
|
|
kwargs: Dict[str, Any] = {}
|
2019-07-29 15:46:48 +02:00
|
|
|
if len(parts) > 2:
|
|
|
|
kwargs["auth_email"] = parts[2]
|
|
|
|
if len(parts) > 3:
|
|
|
|
kwargs["auth_api_key"] = parts[3]
|
2019-08-16 21:17:01 +02:00
|
|
|
kwargs["api_url"] = api_url
|
2021-06-21 12:53:05 +02:00
|
|
|
rendered_example = []
|
|
|
|
for element in get_curl_include_exclude(endpoint, method):
|
|
|
|
kwargs["include"] = None
|
|
|
|
kwargs["exclude"] = None
|
|
|
|
if element["type"] == "include":
|
|
|
|
kwargs["include"] = element["parameters"]["enum"]
|
|
|
|
if element["type"] == "exclude":
|
|
|
|
kwargs["exclude"] = element["parameters"]["enum"]
|
|
|
|
if "description" in element:
|
|
|
|
rendered_example.extend(element["description"].splitlines())
|
|
|
|
rendered_example = rendered_example + generate_curl_example(endpoint, method, **kwargs)
|
|
|
|
return rendered_example
|
2019-07-29 15:46:48 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
SUPPORTED_LANGUAGES: Dict[str, Any] = {
|
2021-02-12 08:20:45 +01:00
|
|
|
"python": {
|
|
|
|
"client_config": PYTHON_CLIENT_CONFIG,
|
|
|
|
"admin_config": PYTHON_CLIENT_ADMIN_CONFIG,
|
|
|
|
"render": render_python_code_example,
|
2019-07-29 15:46:48 +02:00
|
|
|
},
|
2021-02-12 08:20:45 +01:00
|
|
|
"curl": {
|
|
|
|
"render": render_curl_example,
|
2020-05-17 12:04:53 +02:00
|
|
|
},
|
2021-02-12 08:20:45 +01:00
|
|
|
"javascript": {
|
|
|
|
"client_config": JS_CLIENT_CONFIG,
|
|
|
|
"admin_config": JS_CLIENT_ADMIN_CONFIG,
|
|
|
|
"render": render_javascript_code_example,
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
},
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
}
|
2018-01-26 22:08:42 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2020-05-06 00:44:08 +02:00
|
|
|
class APIMarkdownExtension(Extension):
|
2019-08-16 21:17:01 +02:00
|
|
|
def __init__(self, api_url: Optional[str]) -> None:
|
|
|
|
self.config = {
|
2021-02-12 08:20:45 +01:00
|
|
|
"api_url": [
|
2019-08-16 21:17:01 +02:00
|
|
|
api_url,
|
2021-02-12 08:20:45 +01:00
|
|
|
"API URL to use when rendering curl examples",
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
],
|
2019-08-16 21:17:01 +02:00
|
|
|
}
|
|
|
|
|
2023-10-12 19:43:45 +02:00
|
|
|
@override
|
2020-10-20 01:28:13 +02:00
|
|
|
def extendMarkdown(self, md: markdown.Markdown) -> None:
|
|
|
|
md.preprocessors.register(
|
2021-09-17 19:01:36 +02:00
|
|
|
APICodeExamplesPreprocessor(md, self.getConfigs()),
|
|
|
|
"generate_code_example",
|
2024-05-20 22:09:35 +02:00
|
|
|
PREPROCESSOR_PRIORITIES["generate_code_example"],
|
2018-01-26 22:08:42 +01:00
|
|
|
)
|
2020-10-20 01:28:13 +02:00
|
|
|
md.preprocessors.register(
|
2022-07-21 00:01:06 +02:00
|
|
|
APIHeaderPreprocessor(md, self.getConfigs()),
|
|
|
|
"generate_api_header",
|
2024-05-20 22:09:35 +02:00
|
|
|
PREPROCESSOR_PRIORITIES["generate_api_header"],
|
2021-05-22 13:11:23 +02:00
|
|
|
)
|
2021-06-21 21:56:18 +02:00
|
|
|
md.preprocessors.register(
|
|
|
|
ResponseDescriptionPreprocessor(md, self.getConfigs()),
|
|
|
|
"generate_response_description",
|
2024-05-20 22:09:35 +02:00
|
|
|
PREPROCESSOR_PRIORITIES["generate_response_description"],
|
2021-06-21 21:56:18 +02:00
|
|
|
)
|
2021-06-21 22:22:27 +02:00
|
|
|
md.preprocessors.register(
|
|
|
|
ParameterDescriptionPreprocessor(md, self.getConfigs()),
|
|
|
|
"generate_parameter_description",
|
2024-05-20 22:09:35 +02:00
|
|
|
PREPROCESSOR_PRIORITIES["generate_parameter_description"],
|
2021-06-21 22:22:27 +02:00
|
|
|
)
|
2018-01-26 22:08:42 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2021-07-16 08:29:16 +02:00
|
|
|
class BasePreprocessor(Preprocessor):
|
|
|
|
def __init__(
|
2022-11-16 06:15:41 +01:00
|
|
|
self, regexp: Pattern[str], md: markdown.Markdown, config: Mapping[str, Any]
|
2021-07-16 08:29:16 +02:00
|
|
|
) -> None:
|
2020-04-09 21:51:58 +02:00
|
|
|
super().__init__(md)
|
2021-02-12 08:20:45 +01:00
|
|
|
self.api_url = config["api_url"]
|
2022-11-16 06:15:41 +01:00
|
|
|
self.REGEXP = regexp
|
2018-01-26 22:08:42 +01:00
|
|
|
|
2023-10-12 19:43:45 +02:00
|
|
|
@override
|
2018-01-26 22:08:42 +01:00
|
|
|
def run(self, lines: List[str]) -> List[str]:
|
|
|
|
done = False
|
|
|
|
while not done:
|
|
|
|
for line in lines:
|
|
|
|
loc = lines.index(line)
|
2021-07-16 08:29:16 +02:00
|
|
|
match = self.REGEXP.search(line)
|
2018-01-26 22:08:42 +01:00
|
|
|
|
|
|
|
if match:
|
2021-07-16 08:29:16 +02:00
|
|
|
text = self.generate_text(match)
|
2018-01-26 22:08:42 +01:00
|
|
|
|
|
|
|
# The line that contains the directive to include the macro
|
|
|
|
# may be preceded or followed by text or tags, in that case
|
|
|
|
# we need to make sure that any preceding or following text
|
|
|
|
# stays the same.
|
2021-07-16 08:29:16 +02:00
|
|
|
line_split = self.REGEXP.split(line, maxsplit=0)
|
2018-01-26 22:08:42 +01:00
|
|
|
preceding = line_split[0]
|
|
|
|
following = line_split[-1]
|
2020-09-02 06:59:07 +02:00
|
|
|
text = [preceding, *text, following]
|
2021-02-12 08:19:30 +01:00
|
|
|
lines = lines[:loc] + text + lines[loc + 1 :]
|
2018-01-26 22:08:42 +01:00
|
|
|
break
|
|
|
|
else:
|
|
|
|
done = True
|
|
|
|
return lines
|
|
|
|
|
2021-07-16 08:29:16 +02:00
|
|
|
def generate_text(self, match: Match[str]) -> List[str]:
|
2021-09-30 00:10:12 +02:00
|
|
|
function = match.group(1)
|
2021-07-16 08:29:16 +02:00
|
|
|
text = self.render(function)
|
|
|
|
return text
|
2018-01-26 22:08:42 +01:00
|
|
|
|
2021-07-16 08:29:16 +02:00
|
|
|
def render(self, function: str) -> List[str]:
|
2021-10-18 16:30:46 +02:00
|
|
|
raise NotImplementedError("Must be overridden by a child class")
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2021-07-16 08:29:16 +02:00
|
|
|
|
|
|
|
class APICodeExamplesPreprocessor(BasePreprocessor):
|
2020-10-20 02:49:02 +02:00
|
|
|
def __init__(self, md: markdown.Markdown, config: Mapping[str, Any]) -> None:
|
2021-07-16 08:29:16 +02:00
|
|
|
super().__init__(MACRO_REGEXP, md, config)
|
|
|
|
|
2023-10-12 19:43:45 +02:00
|
|
|
@override
|
2021-07-16 08:29:16 +02:00
|
|
|
def generate_text(self, match: Match[str]) -> List[str]:
|
2021-09-30 00:10:12 +02:00
|
|
|
language = match.group(1) or ""
|
|
|
|
function = match.group(2)
|
|
|
|
key = match.group(3)
|
2021-07-16 08:29:16 +02:00
|
|
|
if self.api_url is None:
|
|
|
|
raise AssertionError("Cannot render curl API examples without API URL set.")
|
|
|
|
|
|
|
|
if key == "fixture":
|
|
|
|
text = self.render(function)
|
|
|
|
elif key == "example":
|
|
|
|
path, method = function.rsplit(":", 1)
|
2021-09-30 00:10:12 +02:00
|
|
|
admin_config = language in ADMIN_CONFIG_LANGUAGES and check_requires_administrator(
|
|
|
|
path, method
|
|
|
|
)
|
|
|
|
text = SUPPORTED_LANGUAGES[language]["render"](
|
|
|
|
function, api_url=self.api_url, admin_config=admin_config
|
|
|
|
)
|
2021-07-16 08:29:16 +02:00
|
|
|
return text
|
2020-05-06 00:44:08 +02:00
|
|
|
|
2023-10-12 19:43:45 +02:00
|
|
|
@override
|
2021-07-16 08:29:16 +02:00
|
|
|
def render(self, function: str) -> List[str]:
|
|
|
|
path, method = function.rsplit(":", 1)
|
|
|
|
return generate_openapi_fixture(path, method)
|
2020-05-06 00:44:08 +02:00
|
|
|
|
|
|
|
|
2022-07-21 00:01:06 +02:00
|
|
|
class APIHeaderPreprocessor(BasePreprocessor):
|
2021-07-16 08:29:16 +02:00
|
|
|
def __init__(self, md: markdown.Markdown, config: Mapping[str, Any]) -> None:
|
2022-07-21 00:01:06 +02:00
|
|
|
super().__init__(MACRO_REGEXP_HEADER, md, config)
|
2021-07-16 08:29:16 +02:00
|
|
|
|
2023-10-12 19:43:45 +02:00
|
|
|
@override
|
2021-07-16 08:29:16 +02:00
|
|
|
def render(self, function: str) -> List[str]:
|
2021-02-12 08:20:45 +01:00
|
|
|
path, method = function.rsplit(":", 1)
|
2022-07-21 00:01:06 +02:00
|
|
|
raw_title = get_openapi_summary(path, method)
|
2020-05-06 00:44:08 +02:00
|
|
|
description_dict = get_openapi_description(path, method)
|
2022-07-21 00:01:06 +02:00
|
|
|
return [
|
|
|
|
*("# " + line for line in raw_title.splitlines()),
|
|
|
|
*(["{!api-admin-only.md!}"] if check_requires_administrator(path, method) else []),
|
2022-06-09 05:34:50 +02:00
|
|
|
"",
|
|
|
|
f"`{method.upper()} {self.api_url}/v1{path}`",
|
2022-07-21 00:01:06 +02:00
|
|
|
"",
|
|
|
|
*description_dict.splitlines(),
|
|
|
|
]
|
2021-05-22 13:11:23 +02:00
|
|
|
|
|
|
|
|
2021-07-16 08:29:16 +02:00
|
|
|
class ResponseDescriptionPreprocessor(BasePreprocessor):
|
2021-06-21 21:56:18 +02:00
|
|
|
def __init__(self, md: markdown.Markdown, config: Mapping[str, Any]) -> None:
|
2021-07-16 08:29:16 +02:00
|
|
|
super().__init__(MACRO_REGEXP_RESPONSE_DESC, md, config)
|
2021-06-21 21:56:18 +02:00
|
|
|
|
2023-10-12 19:43:45 +02:00
|
|
|
@override
|
2021-07-16 08:29:16 +02:00
|
|
|
def render(self, function: str) -> List[str]:
|
2021-06-21 21:56:18 +02:00
|
|
|
path, method = function.rsplit(":", 1)
|
|
|
|
raw_description = get_responses_description(path, method)
|
2022-07-21 00:06:16 +02:00
|
|
|
return raw_description.splitlines()
|
2021-06-21 21:56:18 +02:00
|
|
|
|
|
|
|
|
2021-07-16 08:29:16 +02:00
|
|
|
class ParameterDescriptionPreprocessor(BasePreprocessor):
|
2021-06-21 22:22:27 +02:00
|
|
|
def __init__(self, md: markdown.Markdown, config: Mapping[str, Any]) -> None:
|
2021-07-16 08:29:16 +02:00
|
|
|
super().__init__(MACRO_REGEXP_PARAMETER_DESC, md, config)
|
2021-06-21 22:22:27 +02:00
|
|
|
|
2023-10-12 19:43:45 +02:00
|
|
|
@override
|
2021-07-16 08:29:16 +02:00
|
|
|
def render(self, function: str) -> List[str]:
|
2021-06-21 22:22:27 +02:00
|
|
|
path, method = function.rsplit(":", 1)
|
|
|
|
raw_description = get_parameters_description(path, method)
|
2022-07-21 00:06:16 +02:00
|
|
|
return raw_description.splitlines()
|
2021-06-21 22:22:27 +02:00
|
|
|
|
|
|
|
|
2020-05-06 00:44:08 +02:00
|
|
|
def makeExtension(*args: Any, **kwargs: str) -> APIMarkdownExtension:
|
|
|
|
return APIMarkdownExtension(*args, **kwargs)
|