Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ CMakeFiles/
.vs/
scaler/protocol/capnp/*.c++
scaler/protocol/capnp/*.h

docs/source/_static/example_config.toml
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,15 @@ The following table maps each Scaler command to its corresponding section name i

#### Scenario 1: Unified Configuration File

Here is an example of a single `example_config.toml` file that configures multiple components using sections.
To get a complete template with all available options and their default values, you can generate a `example_config.toml` file by running the script included in the `docs` folder:

```bash
python3 docs/source/_scripts/generate_config.py
```

This will create a `example_config.toml` file, which you can then customize.

For simplicity, here is a condensed example of a `example_config.toml` file that configures multiple components:

**example_config.toml**

Expand Down
151 changes: 151 additions & 0 deletions docs/source/_scripts/generate_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import dataclasses
import enum
import importlib
import inspect
import pathlib
import pkgutil
import sys
from typing import Any, Dict, Type, get_args, get_origin, Union, Tuple

from scaler.config.mixins import ConfigType
import scaler.config.section


def find_project_root(marker: str = "pyproject.toml") -> pathlib.Path:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my initial solution was to hardcode this logic but decided to opt for this to make it robust incase directory structure change in future

"""Searches upwards from the current script for a project root marker."""
current_dir = pathlib.Path(__file__).parent.resolve()
while current_dir != current_dir.parent:
if (current_dir / marker).exists():
return current_dir
current_dir = current_dir.parent
raise FileNotFoundError(f"Could not find the project root marker '{marker}'")


try:
SCRIPT_DIR = pathlib.Path(__file__).parent.resolve()
PROJECT_ROOT = find_project_root()
SCALER_ROOT = PROJECT_ROOT / "scaler"
except FileNotFoundError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)


def get_config_classes() -> Dict[str, Type]:
"""Dynamically finds all configuration dataclasses using pkgutil."""
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))

config_classes = {}

for module_info in pkgutil.iter_modules(scaler.config.section.__path__, f"{scaler.config.section.__name__}."):
try:
module = importlib.import_module(module_info.name)
for name, obj in inspect.getmembers(module, inspect.isclass):
if dataclasses.is_dataclass(obj) and obj.__module__ == module_info.name:
section_name = module_info.name.split(".")[-1]
config_classes[section_name] = obj
except ImportError as e:
print(f"Warning: Could not import module {module_info.name}. Skipping. Error: {e}")
return config_classes


def get_type_repr(type_hint: Any) -> str:
"""Creates a user-friendly string representation of a type hint."""
if inspect.isclass(type_hint):
if issubclass(type_hint, ConfigType):
return "str"
if issubclass(type_hint, enum.Enum):
choices = ", ".join(e.name for e in type_hint)
return f"str (choices: {choices})"

origin = get_origin(type_hint)
args = get_args(type_hint)

if origin is Union and len(args) == 2 and args[1] is type(None):
return f"Optional[{get_type_repr(args[0])}]"

if origin in (tuple, Tuple) and len(args) == 2 and args[1] is Ellipsis:
return f"Tuple[{get_type_repr(args[0])}, ...]"

if origin:
origin_name = getattr(origin, "__name__", str(origin)).capitalize()
args_repr = ", ".join(get_type_repr(arg) for arg in args)
return f"{origin_name}[{args_repr}]"

return getattr(type_hint, "__name__", str(type_hint))


def format_value(value: Any) -> str:
"""Formats Python values into valid TOML strings."""
if isinstance(value, (ConfigType, str)):
return f'"{value}"'
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, enum.Enum):
return f'"{value.name}"'
if value is None:
return '""'
if isinstance(value, (list, tuple)):
return f"[{', '.join(format_value(v) for v in value)}]"
if dataclasses.is_dataclass(value):
fields = [f"{f.name} = {format_value(getattr(value, f.name))}" for f in dataclasses.fields(value)]
return f"{{ {', '.join(fields)} }}"
return str(value)


def generate_toml_string(config_classes: Dict[str, Type]) -> str:
"""Generates the full TOML configuration string."""
toml_lines = [
"# Default configuration for OpenGRIS Scaler",
"# This file is auto-generated. Do not edit manually.",
"# For required fields, a value must be provided.",
"",
]

for section_name, config_class in sorted(config_classes.items()):
toml_lines.append(f"[{section_name}]")
for field in dataclasses.fields(config_class):
type_repr = get_type_repr(field.type)

if field.default is dataclasses.MISSING and field.default_factory is dataclasses.MISSING:
toml_lines.append(f"# Type: {type_repr} (REQUIRED)")
toml_lines.append(f'{field.name} = ""')
continue

toml_lines.append(f"# Type: {type_repr}")

default_value = field.default
if field.default_factory is not dataclasses.MISSING:
try:
default_value = field.default_factory()
except Exception as e:
print(f"Warning: Could not execute default_factory for {field.name}. Error: {e}")
continue

formatted_default = format_value(default_value)
toml_lines.append(f"{field.name} = {formatted_default}")
toml_lines.append("")

return "\n".join(toml_lines)


def main():
classes = get_config_classes()
if not classes:
print("Error: No configuration classes found.", file=sys.stderr)
sys.exit(1)

toml_content = generate_toml_string(classes)

output_dir = SCRIPT_DIR.parent / "_static"
output_dir.mkdir(exist_ok=True)
output_path = output_dir / "example_config.toml"

with open(output_path, "w") as f:
f.write(toml_content)

print(f"Successfully generated docs TOML config at: {output_path}")


if __name__ == "__main__":
main()
9 changes: 9 additions & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import os
import sys

import generate_config

sys.path.insert(0, os.path.abspath(os.path.join("..", "..")))


Expand All @@ -31,6 +33,13 @@
.. |release| replace:: {release}
"""

# -- Auto-generate TOML config for docs --------------------------------------
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "_scripts")))

print("Executing script to generate TOML config...")
generate_config.main()
# ---------------------------------------------------------------------------

# -- General configuration ---------------------------------------------------

# Add any Sphinx extension module names here, as strings. They can be
Expand Down
30 changes: 3 additions & 27 deletions docs/source/tutorials/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -190,34 +190,10 @@ Practical Scenarios & Examples

**Scenario 1: Unified Configuration File**

Here is an example of a single ``example_config.toml`` file that configures multiple components using sections.
Here is an example of a single ``example_config.toml`` file with their default values. This file is automatically generated from the source code.

**example_config.toml**

.. code-block:: toml

# This is a unified configuration file for all Scaler components.

[scheduler]
scheduler_address = "tcp://127.0.0.1:6378"
object_storage_address = "tcp://127.0.0.1:6379"
monitor_address = "tcp://127.0.0.1:6380"
allocate_policy = "even"
logging_level = "INFO"
logging_paths = ["/dev/stdout", "/var/log/scaler/scheduler.log"]

[cluster]
scheduler_address = "tcp://127.0.0.1:6378"
num_of_workers = 8
per_worker_capabilities = "linux,cpu=8"
task_timeout_seconds = 600

[object_storage_server]
object_storage_address = "tcp://127.0.0.1:6379"

[webui]
monitor_address = "tcp://127.0.0.1:6380"
web_port = 8081
.. literalinclude:: /_static/example_config.toml
:language: toml


With this single file, starting your entire stack is simple and consistent:
Expand Down
Loading