Add documentation. (#321)

Add documentation that covers:

- Prediction
- Measuremnt
- REST API

Add Python scripts that support automatic documentation generation using the Sphinx
sphinxcontrib.eval extension.

Add automatic update/ test for REST API documentation.

Filter proxy endpoints from REST API documentation.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
Bobby Noelte
2025-01-03 00:31:20 +01:00
committed by GitHub
parent 4cb6dc7270
commit 1866055478
27 changed files with 7565 additions and 6131 deletions

0
scripts/__init__.py Normal file
View File

179
scripts/extract_markdown.py Executable file
View File

@@ -0,0 +1,179 @@
#!.venv/bin/python
r"""This module extracts a part of a markdown string from an input file or a given input string.
The extraction starts at a line that contains the content specified by the `--start-line` parameter
and ends at a line that contains the content specified by the `--end-line` parameter.
If `--start-line` is not specified, extraction starts from the beginning of the file or string.
If `--end-line` is not specified, extraction goes to the end of the file or string.
The extracted markdown string is written either to stdout or to the specified output file.
Additionally, the heading levels can be adjusted by specifying the `--heading-level` parameter.
Usage:
scripts/extract_markdown.py [--input-file INPUT_FILE | --input INPUT_STRING] [--start-line START_LINE] [--end-line END_LINE] [--output-file OUTPUT_FILE] [--heading-level HEADING_LEVEL]
Arguments:
--input-file : The file path to read the markdown content from.
--input : The markdown content as a string.
--start-line : Optional. The string content of the start line from where extraction begins.
--end-line : Optional. The string content of the end line where extraction ends.
--output-file : Optional. The file path to write the extracted markdown content to.
--heading-level: Optional. The number of additional `#` to add to markdown headings or to remove
from markdown headings if negative.
Example:
scripts/extract_markdown.py --input-file input.md --start-line "# Start" --end-line "# End" --output-file output.md --heading-level 1
scripts/extract_markdown.py --input "# Start\n\nSome content here\n\n# End" --start-line "# Start" --end-line "# End" --output-file output.md --heading-level 1
"""
"""
This module extracts a part of a markdown string from an input file or a given input string.
The extraction starts at a line that contains the content specified by the `--start-line` parameter
and ends at a line that contains the content specified by the `--end-line` parameter.
If `--start-line` is not specified, extraction starts from the beginning of the file or string.
If `--end-line` is not specified, extraction goes to the end of the file or string.
The extracted markdown string is written either to stdout or to the specified output file.
Additionally, the heading levels can be adjusted by specifying the `--heading-level` parameter.
Usage:
python extract_markdown.py [--input-file INPUT_FILE | --input INPUT_STRING | --input-stdin] [--start-line START_LINE] [--end-line END_LINE] [--output-file OUTPUT_FILE] [--heading-level HEADING_LEVEL]
Arguments:
--input-file : The file path to read the markdown content from.
--input : The markdown content as a string.
--input-stdin : Read markdown content from stdin.
--start-line : Optional. The string content of the start line from where extraction begins.
--end-line : Optional. The string content of the end line where extraction ends.
--output-file : Optional. The file path to write the extracted markdown content to.
--heading-level: Optional. The number of additional `#` to add to markdown headings or to remove from markdown headings if negative.
Example:
python extract_markdown.py --input-file input.md --start-line "# Start" --end-line "# End" --output-file output.md --heading-level 1
python extract_markdown.py --input "# Start\n\nSome content here\n\n# End" --start-line "# Start" --end-line "# End" --output-file output.md --heading-level 1
"""
import argparse
import re
import sys
def adjust_heading_levels(line: str, heading_level: int) -> str:
"""Adjust the heading levels in a markdown line.
Args:
line (str): The markdown line.
heading_level (int): The number of levels to adjust the headings by.
Returns:
adjusted_line (str): The line with adjusted heading levels.
"""
heading_pattern = re.compile(r"^(#+)\s")
match = heading_pattern.match(line)
if match:
current_level = len(match.group(1))
new_level = current_level + heading_level
if new_level > 0:
adjusted_line = "#" * new_level + line[current_level:]
else:
adjusted_line = line[current_level:]
else:
adjusted_line = line
return adjusted_line
def extract_markdown(content: str, start_line: str, end_line: str, heading_level: int) -> str:
"""Extract a part of a markdown string from given content.
Args:
content (str): The markdown content.
start_line (str): The string content of the start line from where extraction begins.
end_line (str): The string content of the end line where extraction ends.
heading_level (int): The number of levels to adjust the headings by.
Returns:
extracted_content (str): Extracted markdown content as a string.
"""
extracted_content = []
lines = content.splitlines(True)
extracting = start_line is None
for line in lines:
if not extracting and start_line and start_line in line:
extracting = True
extracted_content.append(
adjust_heading_levels(line, heading_level)
) # Include start line in output
continue
if extracting and end_line and end_line in line:
extracting = False
break
if extracting:
extracted_content.append(adjust_heading_levels(line, heading_level))
return "".join(extracted_content)
def main():
"""Main function to run the extraction of the markdown content."""
parser = argparse.ArgumentParser(
description="Extract a part of a markdown string from an input file"
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--input-file", type=str, help="File to read the markdown content from")
group.add_argument("--input", type=str, help="Markdown content as a string")
group.add_argument(
"--input-stdin", action="store_true", help="Read markdown content from stdin"
)
parser.add_argument(
"--start-line",
type=str,
default=None,
help="Optional. The string content of the start line",
)
parser.add_argument(
"--end-line", type=str, default=None, help="Optional. The string content of the end line"
)
parser.add_argument(
"--output-file",
type=str,
default=None,
help="File to write the extracted markdown content to",
)
parser.add_argument(
"--heading-level",
type=int,
default=0,
help="The number of additional `#` to add to markdown headings or to remove from markdown headings if negative",
)
args = parser.parse_args()
try:
if args.input_file:
with open(args.input_file, "r") as f:
content = f.read()
elif args.input:
content = args.input
elif args.input_stdin:
content = sys.stdin.read()
else:
raise ValueError("No valid input source provided.")
extracted_content = extract_markdown(
content, args.start_line, args.end_line, args.heading_level
)
if args.output_file:
# Write to file
with open(args.output_file, "w") as f:
f.write(extracted_content)
else:
# Write to std output
print(extracted_content)
except Exception as e:
print(f"Error during markdown extraction: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

69
scripts/generate_openapi.py Executable file
View File

@@ -0,0 +1,69 @@
#!.venv/bin/python
"""This module generates the OpenAPI specification for the FastAPI application defined in `akkudoktoreos.server.fastapi_server`.
The script can be executed directly to generate the OpenAPI specification
either to the standard output or to a specified file.
Usage:
scripts/generate_openapi.py [--output-file OUTPUT_FILE]
Arguments:
--output-file : Optional. The file path to write the OpenAPI specification to.
Example:
scripts/generate_openapi.py --output-file openapi.json
"""
import argparse
import json
import sys
from fastapi.openapi.utils import get_openapi
from akkudoktoreos.server.fastapi_server import app
def generate_openapi() -> dict:
"""Generate the OpenAPI specification.
Returns:
openapi_spec (dict): OpenAPI specification.
"""
openapi_spec = get_openapi(
title=app.title,
version=app.version,
openapi_version=app.openapi_version,
description=app.description,
routes=app.routes,
)
return openapi_spec
def main():
"""Main function to run the generation of the OpenAPI specification."""
parser = argparse.ArgumentParser(description="Generate OpenAPI Specification")
parser.add_argument(
"--output-file", type=str, default=None, help="File to write the OpenAPI Specification to"
)
args = parser.parse_args()
try:
openapi_spec = generate_openapi()
openapi_spec_str = json.dumps(openapi_spec, indent=2)
if args.output_file:
# Write to file
with open(args.output_file, "w") as f:
f.write(openapi_spec_str)
else:
# Write to std output
print(openapi_spec_str)
except Exception as e:
print(f"Error during OpenAPI specification generation: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

239
scripts/generate_openapi_md.py Executable file
View File

@@ -0,0 +1,239 @@
#!.venv/bin/python
"""Utility functions for OpenAPI specification conversion tasks."""
import argparse
import json
import sys
if __package__ is None or __package__ == "":
# uses current directory visibility
import generate_openapi
else:
# uses current package visibility
from . import generate_openapi
def extract_info(openapi_json: dict) -> dict:
"""Extract basic information from OpenAPI JSON.
Args:
openapi_json (dict): The OpenAPI specification as a Python dictionary.
Returns:
dict: A dictionary containing the title, version, description, and base_url.
"""
info = openapi_json.get("info", {})
servers = openapi_json.get("servers", [{}])
return {
"title": info.get("title", "API Documentation"),
"version": info.get("version", "1.0.0"),
"description": info.get("description", "No description provided."),
"base_url": servers[0].get("url", "No base URL provided."),
}
def format_authentication(security_schemes: dict) -> str:
"""Format the authentication section for the Markdown.
Args:
security_schemes (dict): The security schemes from the OpenAPI spec.
Returns:
str: The formatted authentication section in Markdown.
"""
if not security_schemes:
return ""
markdown = "## Authentication\n\n"
for scheme, details in security_schemes.items():
auth_type = details.get("type", "unknown")
markdown += f"- **{scheme}**: {auth_type}\n\n"
return markdown
def format_parameters(parameters: list) -> str:
"""Format the parameters section for the Markdown.
Args:
parameters (list): The list of parameters from an endpoint.
Returns:
str: The formatted parameters section in Markdown.
"""
if not parameters:
return ""
markdown = "**Parameters**:\n\n"
for param in parameters:
name = param.get("name", "unknown")
location = param.get("in", "unknown")
required = param.get("required", False)
description = param.get("description", "No description provided.")
markdown += (
f"- `{name}` ({location}, {'required' if required else 'optional'}): {description}\n\n"
)
return markdown
def format_request_body(request_body: dict) -> str:
"""Format the request body section for the Markdown.
Args:
request_body (dict): The request body content from an endpoint.
Returns:
str: The formatted request body section in Markdown.
"""
if not request_body:
return ""
markdown = "**Request Body**:\n\n"
for content_type, schema in request_body.items():
markdown += f"- `{content_type}`: {json.dumps(schema.get('schema', {}), indent=2)}\n\n"
return markdown
def format_responses(responses: dict) -> str:
"""Format the responses section for the Markdown.
Args:
responses (dict): The responses from an endpoint.
Returns:
str: The formatted responses section in Markdown.
"""
if not responses:
return ""
markdown = "**Responses**:\n\n"
for status, response in responses.items():
desc = response.get("description", "No description provided.")
markdown += f"- **{status}**: {desc}\n\n"
return markdown
def format_endpoint(path: str, method: str, details: dict) -> str:
"""Format a single endpoint's details for the Markdown.
Args:
path (str): The endpoint path.
method (str): The HTTP method.
details (dict): The details of the endpoint.
Returns:
str: The formatted endpoint section in Markdown.
"""
link_summary = (
details.get("summary", "<summary missing>")
.lower()
.strip()
.replace(" ", "_")
.replace("-", "_")
)
link_path = (
path.lower().strip().replace("/", "_").replace(".", "_").replace("{", "_").replace("}", "_")
)
link_method = f"_{method.lower()})"
# [local](http://localhost:8503/docs#/default/fastapi_config_get_v1_config_get)
local_path = (
"[local](http://localhost:8503/docs#/default/" + link_summary + link_path + link_method
)
# [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_strompreis_strompreis_get)
swagger_path = (
"[swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/"
+ link_summary
+ link_path
+ link_method
)
markdown = f"## {method.upper()} {path}\n\n"
markdown += f"**Links**: {local_path}, {swagger_path}\n\n"
summary = details.get("summary", None)
if summary:
markdown += f"{summary}\n\n"
description = details.get("description", None)
if description:
markdown += "```\n"
markdown += f"{description}"
markdown += "\n```\n\n"
markdown += format_parameters(details.get("parameters", []))
markdown += format_request_body(details.get("requestBody", {}).get("content", {}))
markdown += format_responses(details.get("responses", {}))
markdown += "---\n\n"
return markdown
def openapi_to_markdown(openapi_json: dict) -> str:
"""Convert OpenAPI JSON specification to a Markdown representation.
Args:
openapi_json (dict): The OpenAPI specification as a Python dictionary.
Returns:
str: The Markdown representation of the OpenAPI spec.
"""
info = extract_info(openapi_json)
markdown = f"# {info['title']}\n\n"
markdown += f"**Version**: `{info['version']}`\n\n"
markdown += f"**Description**: {info['description']}\n\n"
markdown += f"**Base URL**: `{info['base_url']}`\n\n"
security_schemes = openapi_json.get("components", {}).get("securitySchemes", {})
markdown += format_authentication(security_schemes)
markdown += "**Endpoints**:\n\n"
paths = openapi_json.get("paths", {})
for path, methods in paths.items():
for method, details in methods.items():
markdown += format_endpoint(path, method, details)
# Assure the is no double \n at end of file
markdown = markdown.rstrip("\n")
markdown += "\n"
return markdown
def generate_openapi_md() -> str:
"""Generate OpenAPI specification in Markdown.
Returns:
str: The Markdown representation of the OpenAPI spec.
"""
openapi_spec = generate_openapi.generate_openapi()
openapi_md = openapi_to_markdown(openapi_spec)
return openapi_md
def main():
"""Main function to run the generation of the OpenAPI specification as Markdown."""
parser = argparse.ArgumentParser(description="Generate OpenAPI Specification as Markdown")
parser.add_argument(
"--output-file", type=str, default=None, help="File to write the OpenAPI Specification to"
)
args = parser.parse_args()
try:
openapi_md = generate_openapi_md()
if args.output_file:
# Write to file
with open(args.output_file, "w") as f:
f.write(openapi_md)
else:
# Write to std output
print(openapi_md)
except Exception as e:
print(f"Error during OpenAPI specification generation: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()