mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2025-10-28 13:26:21 +00:00
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:
0
scripts/__init__.py
Normal file
0
scripts/__init__.py
Normal file
179
scripts/extract_markdown.py
Executable file
179
scripts/extract_markdown.py
Executable 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
69
scripts/generate_openapi.py
Executable 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
239
scripts/generate_openapi_md.py
Executable 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()
|
||||
Reference in New Issue
Block a user