framework-m-core 0.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- framework_m_core/__init__.py +29 -0
- framework_m_core/cli/__init__.py +0 -0
- framework_m_core/cli/config.py +128 -0
- framework_m_core/cli/main.py +30 -0
- framework_m_core/cli/plugin_loader.py +132 -0
- framework_m_core/cli/utility.py +58 -0
- framework_m_core/config.py +171 -0
- framework_m_core/container.py +329 -0
- framework_m_core/decorators.py +308 -0
- framework_m_core/doctypes/__init__.py +51 -0
- framework_m_core/doctypes/activity_log.py +123 -0
- framework_m_core/doctypes/api_key.py +89 -0
- framework_m_core/doctypes/custom_permission.py +118 -0
- framework_m_core/doctypes/document_share.py +142 -0
- framework_m_core/doctypes/email_queue.py +209 -0
- framework_m_core/doctypes/error_log.py +127 -0
- framework_m_core/doctypes/file.py +97 -0
- framework_m_core/doctypes/job_log.py +144 -0
- framework_m_core/doctypes/notification.py +146 -0
- framework_m_core/doctypes/print_format.py +124 -0
- framework_m_core/doctypes/recent_document.py +75 -0
- framework_m_core/doctypes/report.py +94 -0
- framework_m_core/doctypes/scheduled_job.py +97 -0
- framework_m_core/doctypes/session.py +71 -0
- framework_m_core/doctypes/social_account.py +88 -0
- framework_m_core/doctypes/system_settings.py +119 -0
- framework_m_core/doctypes/tenant_translation.py +133 -0
- framework_m_core/doctypes/todo.py +59 -0
- framework_m_core/doctypes/translation.py +107 -0
- framework_m_core/doctypes/user.py +115 -0
- framework_m_core/doctypes/webhook.py +127 -0
- framework_m_core/doctypes/webhook_log.py +114 -0
- framework_m_core/doctypes/workflow.py +51 -0
- framework_m_core/doctypes/workflow_state.py +56 -0
- framework_m_core/doctypes/workflow_transition.py +57 -0
- framework_m_core/domain/__init__.py +1 -0
- framework_m_core/domain/base_controller.py +224 -0
- framework_m_core/domain/base_doctype.py +287 -0
- framework_m_core/domain/mixins.py +131 -0
- framework_m_core/domain/naming_counter.py +50 -0
- framework_m_core/domain/outbox.py +85 -0
- framework_m_core/events/__init__.py +112 -0
- framework_m_core/exceptions.py +144 -0
- framework_m_core/interfaces/__init__.py +23 -0
- framework_m_core/interfaces/audit.py +216 -0
- framework_m_core/interfaces/auth_context.py +165 -0
- framework_m_core/interfaces/authentication.py +86 -0
- framework_m_core/interfaces/base_doctype.py +54 -0
- framework_m_core/interfaces/bootstrap.py +110 -0
- framework_m_core/interfaces/cache.py +140 -0
- framework_m_core/interfaces/controller.py +157 -0
- framework_m_core/interfaces/email_queue.py +187 -0
- framework_m_core/interfaces/email_sender.py +143 -0
- framework_m_core/interfaces/event_bus.py +167 -0
- framework_m_core/interfaces/i18n.py +89 -0
- framework_m_core/interfaces/identity.py +198 -0
- framework_m_core/interfaces/job_queue.py +154 -0
- framework_m_core/interfaces/notification.py +145 -0
- framework_m_core/interfaces/oauth.py +130 -0
- framework_m_core/interfaces/permission.py +141 -0
- framework_m_core/interfaces/print.py +85 -0
- framework_m_core/interfaces/read_model.py +142 -0
- framework_m_core/interfaces/report_engine.py +90 -0
- framework_m_core/interfaces/repository.py +228 -0
- framework_m_core/interfaces/schema_mapper.py +164 -0
- framework_m_core/interfaces/search.py +133 -0
- framework_m_core/interfaces/session.py +167 -0
- framework_m_core/interfaces/socket.py +99 -0
- framework_m_core/interfaces/storage.py +181 -0
- framework_m_core/interfaces/tenant.py +165 -0
- framework_m_core/interfaces/workflow.py +214 -0
- framework_m_core/permission_lookup.py +203 -0
- framework_m_core/permissions.py +228 -0
- framework_m_core/pii.py +215 -0
- framework_m_core/py.typed +0 -0
- framework_m_core/registry.py +312 -0
- framework_m_core/rls.py +201 -0
- framework_m_core/rpc_registry.py +90 -0
- framework_m_core/security.py +44 -0
- framework_m_core/services/__init__.py +12 -0
- framework_m_core/services/user_manager.py +187 -0
- framework_m_core/system_context.py +151 -0
- framework_m_core/types/job_context.py +94 -0
- framework_m_core/unit_of_work.py +138 -0
- framework_m_core-0.4.1.dist-info/METADATA +92 -0
- framework_m_core-0.4.1.dist-info/RECORD +87 -0
- framework_m_core-0.4.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Core module - Domain logic and interfaces."""
|
|
2
|
+
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
|
|
5
|
+
from framework_m_core.container import Container
|
|
6
|
+
from framework_m_core.domain.base_doctype import BaseDocType as DocType
|
|
7
|
+
from framework_m_core.domain.base_doctype import Field
|
|
8
|
+
from framework_m_core.interfaces.base_doctype import BaseDocTypeProtocol
|
|
9
|
+
from framework_m_core.interfaces.bootstrap import BootstrapProtocol
|
|
10
|
+
from framework_m_core.interfaces.controller import BaseControllerProtocol
|
|
11
|
+
from framework_m_core.interfaces.schema_mapper import SchemaMapperProtocol
|
|
12
|
+
from framework_m_core.registry import MetaRegistry
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
__version__ = importlib.metadata.version("framework-m-core")
|
|
16
|
+
except importlib.metadata.PackageNotFoundError:
|
|
17
|
+
__version__ = "0.0.0"
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"BaseControllerProtocol",
|
|
21
|
+
"BaseDocTypeProtocol",
|
|
22
|
+
"BootstrapProtocol",
|
|
23
|
+
"Container",
|
|
24
|
+
"DocType",
|
|
25
|
+
"Field",
|
|
26
|
+
"MetaRegistry",
|
|
27
|
+
"SchemaMapperProtocol",
|
|
28
|
+
"__version__",
|
|
29
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Configuration Management CLI Commands.
|
|
2
|
+
|
|
3
|
+
This module provides CLI commands for managing Framework M configuration:
|
|
4
|
+
- m config:show: Display current configuration
|
|
5
|
+
- m config:set <key> <value>: Update configuration value
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
m config:show # Show all config
|
|
9
|
+
m config:set framework.name myapp
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Annotated, Any
|
|
16
|
+
|
|
17
|
+
import cyclopts
|
|
18
|
+
|
|
19
|
+
from framework_m_core.config import (
|
|
20
|
+
CONFIG_FILE_NAME,
|
|
21
|
+
DEFAULT_CONFIG,
|
|
22
|
+
find_config_file,
|
|
23
|
+
format_config,
|
|
24
|
+
get_nested_value,
|
|
25
|
+
load_config,
|
|
26
|
+
save_config,
|
|
27
|
+
set_nested_value,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# =============================================================================
|
|
31
|
+
# CLI Commands
|
|
32
|
+
# =============================================================================
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def config_show_command(
|
|
36
|
+
key: Annotated[
|
|
37
|
+
str | None,
|
|
38
|
+
cyclopts.Parameter(help="Specific key to show (e.g., framework.name)"),
|
|
39
|
+
] = None,
|
|
40
|
+
config_file: Annotated[
|
|
41
|
+
Path | None,
|
|
42
|
+
cyclopts.Parameter(name="--config", help="Path to config file"),
|
|
43
|
+
] = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Display current configuration.
|
|
46
|
+
|
|
47
|
+
Shows the current Framework M configuration from framework_config.toml.
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
m config:show # Show all config
|
|
51
|
+
m config:show framework.name # Show specific key
|
|
52
|
+
"""
|
|
53
|
+
config_path = config_file or find_config_file()
|
|
54
|
+
config = load_config(config_path)
|
|
55
|
+
|
|
56
|
+
if not config:
|
|
57
|
+
print(f"No config found at: {config_path}")
|
|
58
|
+
print()
|
|
59
|
+
print("Create a config file with default values:")
|
|
60
|
+
print(" m config:set framework.name myapp")
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
print(f"Config file: {config_path}")
|
|
64
|
+
print()
|
|
65
|
+
|
|
66
|
+
if key:
|
|
67
|
+
value = get_nested_value(config, key)
|
|
68
|
+
if value is not None:
|
|
69
|
+
print(f"{key} = {value}")
|
|
70
|
+
else:
|
|
71
|
+
print(f"Key not found: {key}")
|
|
72
|
+
raise SystemExit(1)
|
|
73
|
+
else:
|
|
74
|
+
print(format_config(config))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def config_set_command(
|
|
78
|
+
key: Annotated[str, cyclopts.Parameter(help="Key to set (e.g., framework.name)")],
|
|
79
|
+
value: Annotated[str, cyclopts.Parameter(help="Value to set")],
|
|
80
|
+
config_file: Annotated[
|
|
81
|
+
Path | None,
|
|
82
|
+
cyclopts.Parameter(name="--config", help="Path to config file"),
|
|
83
|
+
] = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Set a configuration value.
|
|
86
|
+
|
|
87
|
+
Updates a value in framework_config.toml, creating the file if needed.
|
|
88
|
+
|
|
89
|
+
Examples:
|
|
90
|
+
m config:set framework.name myapp
|
|
91
|
+
m config:set apps.installed "myapp,otherapp"
|
|
92
|
+
"""
|
|
93
|
+
config_path = config_file or find_config_file()
|
|
94
|
+
config = load_config(config_path) or DEFAULT_CONFIG.copy()
|
|
95
|
+
|
|
96
|
+
# Parse value (handle lists)
|
|
97
|
+
if "," in value:
|
|
98
|
+
parsed_value: Any = [v.strip() for v in value.split(",")]
|
|
99
|
+
else:
|
|
100
|
+
parsed_value = value
|
|
101
|
+
|
|
102
|
+
old_value = get_nested_value(config, key)
|
|
103
|
+
set_nested_value(config, key, parsed_value)
|
|
104
|
+
|
|
105
|
+
save_config(config_path, config)
|
|
106
|
+
|
|
107
|
+
print(f"Config file: {config_path}")
|
|
108
|
+
print()
|
|
109
|
+
if old_value is not None:
|
|
110
|
+
print(f" {key}: {old_value} -> {parsed_value}")
|
|
111
|
+
else:
|
|
112
|
+
print(f" {key} = {parsed_value}")
|
|
113
|
+
print()
|
|
114
|
+
print("✓ Configuration updated")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
__all__ = [
|
|
118
|
+
"CONFIG_FILE_NAME",
|
|
119
|
+
"DEFAULT_CONFIG",
|
|
120
|
+
"config_set_command",
|
|
121
|
+
"config_show_command",
|
|
122
|
+
"find_config_file",
|
|
123
|
+
"format_config",
|
|
124
|
+
"get_nested_value",
|
|
125
|
+
"load_config",
|
|
126
|
+
"save_config",
|
|
127
|
+
"set_nested_value",
|
|
128
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import cyclopts
|
|
2
|
+
|
|
3
|
+
from framework_m_core import __version__
|
|
4
|
+
from framework_m_core.cli.config import config_set_command, config_show_command
|
|
5
|
+
from framework_m_core.cli.plugin_loader import load_plugins
|
|
6
|
+
from framework_m_core.cli.utility import info_command
|
|
7
|
+
|
|
8
|
+
app = cyclopts.App(
|
|
9
|
+
name="m",
|
|
10
|
+
version=__version__,
|
|
11
|
+
help="Framework M CLI",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Register core commands
|
|
15
|
+
app.command(info_command, name="info")
|
|
16
|
+
|
|
17
|
+
# Config commands
|
|
18
|
+
app.command(config_show_command, name="config:show")
|
|
19
|
+
app.command(config_set_command, name="config:set")
|
|
20
|
+
|
|
21
|
+
# Load plugins
|
|
22
|
+
load_plugins(app)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main() -> None:
|
|
26
|
+
app()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
main()
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Plugin loader for CLI extensibility.
|
|
2
|
+
|
|
3
|
+
This module provides the mechanism for 3rd-party apps to register
|
|
4
|
+
CLI commands via entry points. Apps can register either cyclopts Apps
|
|
5
|
+
(sub-command groups) or standalone functions as commands.
|
|
6
|
+
|
|
7
|
+
Entry Point Group: framework_m_core.cli_commands
|
|
8
|
+
|
|
9
|
+
Example pyproject.toml:
|
|
10
|
+
[project.entry-points."framework_m_core.cli_commands"]
|
|
11
|
+
studio = "my_app.cli:studio_app" # cyclopts.App
|
|
12
|
+
custom = "my_app.cli:custom_command" # Function
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
>>> app = cyclopts.App()
|
|
16
|
+
>>> load_plugins(app) # Discovers and registers all plugins
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
from importlib.metadata import entry_points
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
24
|
+
|
|
25
|
+
import cyclopts
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from importlib.metadata import EntryPoint
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
# Entry point group for CLI command plugins
|
|
33
|
+
CLI_COMMANDS_GROUP = "framework_m_core.cli_commands"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def discover_plugins() -> list[EntryPoint]:
|
|
37
|
+
"""Discover all registered CLI command plugins.
|
|
38
|
+
|
|
39
|
+
Scans the entry points registered under the 'framework_m_core.cli_commands'
|
|
40
|
+
group. Each entry point should point to either:
|
|
41
|
+
- A `cyclopts.App` instance (registered as sub-command group)
|
|
42
|
+
- A callable function (registered as single command)
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
List of entry points for CLI plugins.
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
>>> plugins = discover_plugins()
|
|
49
|
+
>>> for ep in plugins:
|
|
50
|
+
... print(f"Found plugin: {ep.name}")
|
|
51
|
+
"""
|
|
52
|
+
eps = entry_points()
|
|
53
|
+
|
|
54
|
+
# Python 3.12+ uses .select()
|
|
55
|
+
return list(eps.select(group=CLI_COMMANDS_GROUP))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def register_plugins(app: cyclopts.App, plugins: list[EntryPoint]) -> None:
|
|
59
|
+
"""Register discovered plugins with the main cyclopts app.
|
|
60
|
+
|
|
61
|
+
For each plugin entry point:
|
|
62
|
+
- If it's a cyclopts.App: register as sub-command group
|
|
63
|
+
- If it's a callable: register as standalone command
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
app: The main cyclopts application instance.
|
|
67
|
+
plugins: List of entry points to register.
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
>>> app = cyclopts.App()
|
|
71
|
+
>>> plugins = discover_plugins()
|
|
72
|
+
>>> register_plugins(app, plugins)
|
|
73
|
+
"""
|
|
74
|
+
for ep in plugins:
|
|
75
|
+
try:
|
|
76
|
+
plugin = ep.load()
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.warning(
|
|
79
|
+
f"Failed to load CLI plugin '{ep.name}': {e}",
|
|
80
|
+
exc_info=True,
|
|
81
|
+
)
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
if isinstance(plugin, cyclopts.App):
|
|
86
|
+
# Register cyclopts App as sub-command group
|
|
87
|
+
app.command(plugin, name=ep.name)
|
|
88
|
+
logger.debug(f"Registered cyclopts App plugin: {ep.name}")
|
|
89
|
+
elif callable(plugin):
|
|
90
|
+
# Register function as command
|
|
91
|
+
app.command(plugin, name=ep.name)
|
|
92
|
+
logger.debug(f"Registered function plugin: {ep.name}")
|
|
93
|
+
else:
|
|
94
|
+
logger.warning(
|
|
95
|
+
f"Plugin '{ep.name}' is not a cyclopts App or callable, skipping"
|
|
96
|
+
)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.warning(
|
|
99
|
+
f"Failed to register CLI plugin '{ep.name}': {e}",
|
|
100
|
+
exc_info=True,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def load_plugins(app: cyclopts.App) -> None:
|
|
105
|
+
"""Discover and register all CLI plugins.
|
|
106
|
+
|
|
107
|
+
Convenience function that combines discovery and registration.
|
|
108
|
+
Call this during app initialization to enable all plugins.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
app: The main cyclopts application instance.
|
|
112
|
+
|
|
113
|
+
Example:
|
|
114
|
+
>>> app = cyclopts.App(name="m")
|
|
115
|
+
>>> load_plugins(app) # All plugins now registered
|
|
116
|
+
>>> app() # Run CLI
|
|
117
|
+
"""
|
|
118
|
+
plugins = discover_plugins()
|
|
119
|
+
|
|
120
|
+
if plugins:
|
|
121
|
+
logger.info(f"Discovered {len(plugins)} CLI plugin(s)")
|
|
122
|
+
register_plugins(app, plugins)
|
|
123
|
+
else:
|
|
124
|
+
logger.debug("No CLI plugins discovered")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
__all__ = [
|
|
128
|
+
"CLI_COMMANDS_GROUP",
|
|
129
|
+
"discover_plugins",
|
|
130
|
+
"load_plugins",
|
|
131
|
+
"register_plugins",
|
|
132
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Utility commands for Framework M CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import platform
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import cyclopts
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from framework_m_core import __version__
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_python_version() -> str:
|
|
19
|
+
"""Get Python version string."""
|
|
20
|
+
return platform.python_version()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_framework_version() -> str:
|
|
24
|
+
"""Get Framework M version string."""
|
|
25
|
+
return __version__
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_pythonpath() -> str:
|
|
29
|
+
"""Get PYTHONPATH."""
|
|
30
|
+
return sys.path[0]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def info_command(
|
|
34
|
+
verbose: Annotated[
|
|
35
|
+
bool, cyclopts.Parameter(name="--verbose", help="Show detailed info")
|
|
36
|
+
] = False,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Show system and framework information."""
|
|
39
|
+
table = Table(title="Framework M Information")
|
|
40
|
+
table.add_column("Key", style="cyan")
|
|
41
|
+
table.add_column("Value", style="green")
|
|
42
|
+
|
|
43
|
+
table.add_row("Framework M", get_framework_version())
|
|
44
|
+
table.add_row("Python", get_python_version())
|
|
45
|
+
|
|
46
|
+
if verbose:
|
|
47
|
+
table.add_row("Platform", platform.platform())
|
|
48
|
+
table.add_row("Services", "None (Mock)")
|
|
49
|
+
|
|
50
|
+
console.print(table)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"get_framework_version",
|
|
55
|
+
"get_python_version",
|
|
56
|
+
"get_pythonpath",
|
|
57
|
+
"info_command",
|
|
58
|
+
]
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Core Configuration Management.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for finding and loading the framework configuration.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
# Default config file name
|
|
12
|
+
CONFIG_FILE_NAME = "framework_config.toml"
|
|
13
|
+
|
|
14
|
+
# Default config structure
|
|
15
|
+
DEFAULT_CONFIG = {
|
|
16
|
+
"framework": {
|
|
17
|
+
"name": "my_app",
|
|
18
|
+
"version": "0.1.0",
|
|
19
|
+
},
|
|
20
|
+
"apps": {
|
|
21
|
+
"installed": [],
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def find_config_file() -> Path:
|
|
27
|
+
"""Find the config file in current directory or parents.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Path to config file (may not exist)
|
|
31
|
+
"""
|
|
32
|
+
for path in [Path.cwd(), *Path.cwd().parents]:
|
|
33
|
+
config_path = path / CONFIG_FILE_NAME
|
|
34
|
+
if config_path.exists():
|
|
35
|
+
return config_path
|
|
36
|
+
|
|
37
|
+
# Default to cwd if not found
|
|
38
|
+
return Path.cwd() / CONFIG_FILE_NAME
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def load_config(config_path: Path | None = None) -> dict[str, Any]:
|
|
42
|
+
"""Load configuration from TOML file.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
config_path: Path to config file (auto-detect if None)
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Configuration dict
|
|
49
|
+
"""
|
|
50
|
+
if config_path is None:
|
|
51
|
+
config_path = find_config_file()
|
|
52
|
+
|
|
53
|
+
if not config_path.exists():
|
|
54
|
+
return {}
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
import tomllib
|
|
58
|
+
|
|
59
|
+
return tomllib.loads(config_path.read_text())
|
|
60
|
+
except Exception:
|
|
61
|
+
return {}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_nested_value(config: dict[str, Any], key: str) -> Any | None:
|
|
65
|
+
"""Get a nested value from config using dot notation.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
config: Configuration dict
|
|
69
|
+
key: Dot-separated key (e.g., "framework.name")
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Value or None if not found
|
|
73
|
+
"""
|
|
74
|
+
parts = key.split(".")
|
|
75
|
+
current = config
|
|
76
|
+
|
|
77
|
+
for part in parts:
|
|
78
|
+
if isinstance(current, dict) and part in current:
|
|
79
|
+
current = current[part]
|
|
80
|
+
else:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
return current
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def set_nested_value(config: dict[str, Any], key: str, value: Any) -> None:
|
|
87
|
+
"""Set a nested value in config using dot notation.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
config: Configuration dict to modify
|
|
91
|
+
key: Dot-separated key (e.g., "framework.name")
|
|
92
|
+
value: Value to set
|
|
93
|
+
"""
|
|
94
|
+
parts = key.split(".")
|
|
95
|
+
current = config
|
|
96
|
+
|
|
97
|
+
# Navigate/create nested dicts
|
|
98
|
+
for part in parts[:-1]:
|
|
99
|
+
if part not in current:
|
|
100
|
+
current[part] = {}
|
|
101
|
+
current = current[part]
|
|
102
|
+
|
|
103
|
+
# Set the final value
|
|
104
|
+
current[parts[-1]] = value
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def save_config(config_path: Path, config: dict[str, Any]) -> None:
|
|
108
|
+
"""Save configuration to TOML file.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
config_path: Path to config file
|
|
112
|
+
config: Configuration dict to save
|
|
113
|
+
"""
|
|
114
|
+
# Simple TOML writing (no external dependencies)
|
|
115
|
+
lines: list[str] = []
|
|
116
|
+
for section, values in config.items():
|
|
117
|
+
lines.append(f"[{section}]")
|
|
118
|
+
if isinstance(values, dict):
|
|
119
|
+
for key, value in values.items():
|
|
120
|
+
if isinstance(value, str):
|
|
121
|
+
lines.append(f'{key} = "{value}"')
|
|
122
|
+
elif isinstance(value, list):
|
|
123
|
+
items = ", ".join(f'"{v}"' for v in value)
|
|
124
|
+
lines.append(f"{key} = [{items}]")
|
|
125
|
+
elif isinstance(value, bool):
|
|
126
|
+
lines.append(f"{key} = {str(value).lower()}")
|
|
127
|
+
elif isinstance(value, (int, float)):
|
|
128
|
+
lines.append(f"{key} = {value}")
|
|
129
|
+
else:
|
|
130
|
+
lines.append(f'{key} = "{value}"')
|
|
131
|
+
lines.append("")
|
|
132
|
+
|
|
133
|
+
config_path.write_text("\n".join(lines))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def format_config(config: dict[str, Any], indent: int = 0) -> str:
|
|
137
|
+
"""Format config dict for display.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
config: Configuration dict
|
|
141
|
+
indent: Indentation level
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Formatted string
|
|
145
|
+
"""
|
|
146
|
+
lines = []
|
|
147
|
+
prefix = " " * indent
|
|
148
|
+
|
|
149
|
+
for key, value in config.items():
|
|
150
|
+
if isinstance(value, dict):
|
|
151
|
+
lines.append(f"{prefix}[{key}]")
|
|
152
|
+
lines.append(format_config(value, indent + 1))
|
|
153
|
+
elif isinstance(value, list):
|
|
154
|
+
items = ", ".join(str(v) for v in value)
|
|
155
|
+
lines.append(f"{prefix}{key} = [{items}]")
|
|
156
|
+
else:
|
|
157
|
+
lines.append(f"{prefix}{key} = {value}")
|
|
158
|
+
|
|
159
|
+
return "\n".join(lines)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
__all__ = [
|
|
163
|
+
"CONFIG_FILE_NAME",
|
|
164
|
+
"DEFAULT_CONFIG",
|
|
165
|
+
"find_config_file",
|
|
166
|
+
"format_config",
|
|
167
|
+
"get_nested_value",
|
|
168
|
+
"load_config",
|
|
169
|
+
"save_config",
|
|
170
|
+
"set_nested_value",
|
|
171
|
+
]
|