Files
EOS/scripts/generate_openapi.py
T
Bobby NoelteandClaude Opus 5.5 57132df44e fix(config): enable documentation mode only explicitly
ConfigEOS.documentation_mode() was true as soon as Sphinx was imported
anywhere in the process. A test importing Sphinx therefore switched all
later tests into documentation mode, and test_config_md_current failed
depending on test order. Documentation mode is now enabled only by the
documentation generators (_force_documentation_mode) or by the Sphinx
configuration, which sets sys._called_from_sphinx in docs/conf.py.

Declare _force_documentation_mode as a ClassVar. Without the annotation
pydantic turned it into a private attribute, and the class attribute was
a truthy descriptor object.

Behavior change: because of that descriptor, every running EOS was in
documentation mode, so the get_adapter/config/measurement/prediction/ems/
database/resource_registry() guards never raised "access before init" and
created the singletons lazily on first access. They now raise for every
access before initialization. The worker process of uvicorn's auto-reload
(server.reload, make run-dev) relied on that: the app's lifespan now sets up
the configuration and initializes the singletons that do not exist yet.

Leave runtime state out of the documentation: the Home Assistant entity ID
properties and OptimizationCommonSettings.keys return an empty list in
documentation mode, so entity IDs and solution keys of a running system
no longer reach the generated configuration docs.

Keep the tests isolated: test_doc sets documentation mode on the class
and test_docstringrst restores sys._called_from_sphinx after reading the
Sphinx configuration. Add tests for how documentation mode is enabled and
for the runtime values it leaves out.

Additional changes
- Generate openapi.json in documentation mode, as the config docs.
- Share the documentation mode check as coreabc.documentation_mode().
- Move the logging and server address setup of run_eos() to setup_eos_config().
- Test that EOS starts with server.reload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 12:40:15 +01:00

105 lines
3.4 KiB
Python
Executable File

#!.venv/bin/python
"""This module generates the OpenAPI specification for the EOS application defined in `akkudoktoreos.server.eos`.
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 os
import sys
from pathlib import Path
# Add the src directory to sys.path so import akkudoktoreos works in all cases
PROJECT_ROOT = Path(__file__).parent.parent
SRC_DIR = PROJECT_ROOT / "src"
sys.path.insert(0, str(SRC_DIR))
from akkudoktoreos.config.config import ConfigEOS
from akkudoktoreos.core.coreabc import get_config
from akkudoktoreos.server.eos import app
def generate_openapi() -> dict:
# Documentation mode: leave runtime state (e.g. optimization solution keys,
# Home Assistant entity IDs) out of the specification.
force_documentation_mode = ConfigEOS._force_documentation_mode
ConfigEOS._force_documentation_mode = True
try:
# Make minimal config to make the generation reproducable
config_eos = get_config(init={
"with_init_settings": True,
"with_env_settings": False,
"with_dotenv_settings": False,
"with_file_settings": False,
"with_file_secret_settings": False,
})
openapi_spec = app.openapi()
finally:
ConfigEOS._force_documentation_mode = force_documentation_mode
config_schema = (
openapi_spec
.get("components", {})
.get("schemas", {})
.get("ConfigEOS", {})
.get("properties", {})
)
# ---- General settings ----
general = config_schema.get("general", {}).get("default")
if general:
general.update({
"config_file_path": "/home/user/.config/net.akkudoktoreos.net/EOS.config.json",
"config_folder_path": "/home/user/.config/net.akkudoktoreos.net",
"data_folder_path": "/home/user/.local/share/net.akkudoktoreos.net",
"data_output_path": "/home/user/.local/share/net.akkudoktoreos.net/output",
})
# ---- Logging settings ----
logging_cfg = config_schema.get("logging", {}).get("default")
if logging_cfg:
logging_cfg["file_path"] = "/home/user/.local/share/net.akkudoktoreos.net/output/eos.log"
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", encoding="utf-8", newline="\n") 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()