agentdag 0.0.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.
- agentdag/__init__.py +29 -0
- agentdag/__init__conf__.py +90 -0
- agentdag/__main__.py +9 -0
- agentdag/adapters/__init__.py +15 -0
- agentdag/adapters/cli/__init__.py +66 -0
- agentdag/adapters/cli/commands/__init__.py +30 -0
- agentdag/adapters/cli/commands/config.py +352 -0
- agentdag/adapters/cli/commands/email/__init__.py +16 -0
- agentdag/adapters/cli/commands/email/_common.py +288 -0
- agentdag/adapters/cli/commands/email/send_email.py +140 -0
- agentdag/adapters/cli/commands/email/send_notification.py +99 -0
- agentdag/adapters/cli/commands/info.py +75 -0
- agentdag/adapters/cli/commands/logging.py +34 -0
- agentdag/adapters/cli/constants.py +28 -0
- agentdag/adapters/cli/context.py +156 -0
- agentdag/adapters/cli/exit_codes.py +52 -0
- agentdag/adapters/cli/main.py +119 -0
- agentdag/adapters/cli/py.typed +0 -0
- agentdag/adapters/cli/root.py +165 -0
- agentdag/adapters/cli/safe_console.py +213 -0
- agentdag/adapters/cli/typed_click.py +51 -0
- agentdag/adapters/config/__init__.py +25 -0
- agentdag/adapters/config/defaultconfig.d/40-layered-config.toml +71 -0
- agentdag/adapters/config/defaultconfig.d/50-mail.toml +222 -0
- agentdag/adapters/config/defaultconfig.d/90-logging.toml +432 -0
- agentdag/adapters/config/defaultconfig.toml +46 -0
- agentdag/adapters/config/deploy.py +122 -0
- agentdag/adapters/config/display.py +60 -0
- agentdag/adapters/config/loader.py +198 -0
- agentdag/adapters/config/overrides.py +191 -0
- agentdag/adapters/config/permissions.py +186 -0
- agentdag/adapters/config/py.typed +0 -0
- agentdag/adapters/email/__init__.py +28 -0
- agentdag/adapters/email/config.py +311 -0
- agentdag/adapters/email/py.typed +0 -0
- agentdag/adapters/email/sender.py +26 -0
- agentdag/adapters/email/transport.py +287 -0
- agentdag/adapters/email/validation.py +65 -0
- agentdag/adapters/logging/__init__.py +13 -0
- agentdag/adapters/logging/py.typed +0 -0
- agentdag/adapters/logging/setup.py +134 -0
- agentdag/adapters/memory/__init__.py +50 -0
- agentdag/adapters/memory/config.py +65 -0
- agentdag/adapters/memory/email.py +181 -0
- agentdag/adapters/memory/logging.py +18 -0
- agentdag/adapters/py.typed +0 -0
- agentdag/application/__init__.py +32 -0
- agentdag/application/ports.py +115 -0
- agentdag/application/py.typed +0 -0
- agentdag/composition/__init__.py +128 -0
- agentdag/composition/py.typed +0 -0
- agentdag/domain/__init__.py +32 -0
- agentdag/domain/behaviors.py +28 -0
- agentdag/domain/enums.py +55 -0
- agentdag/domain/errors.py +56 -0
- agentdag/domain/py.typed +0 -0
- agentdag/entry.py +26 -0
- agentdag/py.typed +0 -0
- agentdag-0.0.1.dist-info/METADATA +430 -0
- agentdag-0.0.1.dist-info/RECORD +63 -0
- agentdag-0.0.1.dist-info/WHEEL +4 -0
- agentdag-0.0.1.dist-info/entry_points.txt +2 -0
- agentdag-0.0.1.dist-info/licenses/LICENSE +22 -0
agentdag/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Public package surface exposing greeting, metadata, and configuration.
|
|
2
|
+
|
|
3
|
+
This module provides the stable public API for the package, routing imports
|
|
4
|
+
through the proper architectural layers:
|
|
5
|
+
- Domain exports: Core business logic (greeting)
|
|
6
|
+
- Composition exports: Wired adapter services (configuration)
|
|
7
|
+
- Metadata: Package information
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
# Metadata
|
|
13
|
+
from .__init__conf__ import print_info
|
|
14
|
+
|
|
15
|
+
# Composition exports (wired adapters)
|
|
16
|
+
from .composition import get_config
|
|
17
|
+
|
|
18
|
+
# Domain exports
|
|
19
|
+
from .domain.behaviors import (
|
|
20
|
+
CANONICAL_GREETING,
|
|
21
|
+
build_greeting,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"CANONICAL_GREETING",
|
|
26
|
+
"build_greeting",
|
|
27
|
+
"get_config",
|
|
28
|
+
"print_info",
|
|
29
|
+
]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Static package metadata surfaced to CLI commands and documentation.
|
|
2
|
+
|
|
3
|
+
Purpose
|
|
4
|
+
-------
|
|
5
|
+
Expose the current project metadata as simple constants. These values are kept
|
|
6
|
+
in sync with ``pyproject.toml`` by development automation (tests, push
|
|
7
|
+
pipelines), so runtime code does not query packaging metadata.
|
|
8
|
+
|
|
9
|
+
Contents
|
|
10
|
+
--------
|
|
11
|
+
* Module-level constants describing the published package.
|
|
12
|
+
* :func:`print_info` rendering the constants for the CLI ``info`` command.
|
|
13
|
+
|
|
14
|
+
System Role
|
|
15
|
+
-----------
|
|
16
|
+
Lives in the adapters/platform layer; CLI transports import these constants to
|
|
17
|
+
present authoritative project information without invoking packaging APIs.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"LAYEREDCONF_APP",
|
|
26
|
+
"LAYEREDCONF_SLUG",
|
|
27
|
+
"LAYEREDCONF_VENDOR",
|
|
28
|
+
"author",
|
|
29
|
+
"author_email",
|
|
30
|
+
"homepage",
|
|
31
|
+
"name",
|
|
32
|
+
"print_info",
|
|
33
|
+
"shell_command",
|
|
34
|
+
"title",
|
|
35
|
+
"version",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
#: Distribution name declared in ``pyproject.toml``.
|
|
39
|
+
name = "agentdag"
|
|
40
|
+
#: Human-readable summary shown in CLI help output.
|
|
41
|
+
title = "Coordinator that runs a small graph of AI-agent nodes with bounded spend, mechanical gates and journaled state"
|
|
42
|
+
#: Current release version pulled from ``pyproject.toml`` by automation.
|
|
43
|
+
version = "0.0.1"
|
|
44
|
+
#: Repository homepage presented to users.
|
|
45
|
+
homepage = "https://github.com/bitranox/agentdag"
|
|
46
|
+
#: Author attribution surfaced in CLI output.
|
|
47
|
+
author = "bitranox"
|
|
48
|
+
#: Contact email surfaced in CLI output.
|
|
49
|
+
author_email = "bitranox@gmail.com"
|
|
50
|
+
#: Console-script name published by the package.
|
|
51
|
+
shell_command = "agentdag"
|
|
52
|
+
|
|
53
|
+
#: Vendor identifier for lib_layered_config paths (macOS/Windows)
|
|
54
|
+
LAYEREDCONF_VENDOR: str = "bitranox"
|
|
55
|
+
#: Application display name for lib_layered_config paths (macOS/Windows)
|
|
56
|
+
LAYEREDCONF_APP: str = "agentdag"
|
|
57
|
+
#: Configuration slug for lib_layered_config Linux paths and environment variables
|
|
58
|
+
LAYEREDCONF_SLUG: str = "agentdag"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def print_info() -> None:
|
|
62
|
+
"""Print the summarised metadata block used by the CLI ``info`` command.
|
|
63
|
+
|
|
64
|
+
Why
|
|
65
|
+
Provides a single, auditable rendering function so documentation and
|
|
66
|
+
CLI output always match the system design reference.
|
|
67
|
+
|
|
68
|
+
Side Effects
|
|
69
|
+
Writes to ``stdout``.
|
|
70
|
+
|
|
71
|
+
Examples
|
|
72
|
+
--------
|
|
73
|
+
>>> print_info() # doctest: +ELLIPSIS
|
|
74
|
+
Info for agentdag:
|
|
75
|
+
...
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
fields = [
|
|
79
|
+
("name", name),
|
|
80
|
+
("title", title),
|
|
81
|
+
("version", version),
|
|
82
|
+
("homepage", homepage),
|
|
83
|
+
("author", author),
|
|
84
|
+
("author_email", author_email),
|
|
85
|
+
("shell_command", shell_command),
|
|
86
|
+
]
|
|
87
|
+
pad = max(len(label) for label, _ in fields)
|
|
88
|
+
lines = [f"Info for {name}:", ""]
|
|
89
|
+
lines.extend(f" {label.ljust(pad)} = {value}" for label, value in fields)
|
|
90
|
+
sys.stdout.write("\n".join(lines) + "\n")
|
agentdag/__main__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Module entry point for ``python -m agentdag``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .adapters.cli.main import main
|
|
6
|
+
from .composition import build_production
|
|
7
|
+
|
|
8
|
+
if __name__ == "__main__":
|
|
9
|
+
raise SystemExit(main(services_factory=build_production))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Adapters layer - infrastructure and framework integrations.
|
|
2
|
+
|
|
3
|
+
Contains adapter implementations that connect the application to external
|
|
4
|
+
systems and frameworks (CLI, configuration, email, logging).
|
|
5
|
+
|
|
6
|
+
Contents:
|
|
7
|
+
* :mod:`.config` - Configuration loading, deployment, and display
|
|
8
|
+
* :mod:`.email` - Email sending via SMTP
|
|
9
|
+
* :mod:`.logging` - Logging setup with lib_log_rich
|
|
10
|
+
* :mod:`.cli` - Click CLI framework integration
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""CLI package providing the command-line interface.
|
|
2
|
+
|
|
3
|
+
Re-exports all public symbols from submodules for convenient access.
|
|
4
|
+
|
|
5
|
+
Contents:
|
|
6
|
+
* Click context helpers and traceback state management from :mod:`.context`
|
|
7
|
+
* Root command group from :mod:`.root`
|
|
8
|
+
* Entry point from :mod:`.main`
|
|
9
|
+
* All command functions from :mod:`.commands`
|
|
10
|
+
|
|
11
|
+
System Role:
|
|
12
|
+
Acts as the public facade for the CLI subsystem. Consumers import from here
|
|
13
|
+
and remain insulated from internal module boundaries.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from .commands import (
|
|
19
|
+
cli_config,
|
|
20
|
+
cli_config_deploy,
|
|
21
|
+
cli_config_generate_examples,
|
|
22
|
+
cli_fail,
|
|
23
|
+
cli_hello,
|
|
24
|
+
cli_info,
|
|
25
|
+
cli_logdemo,
|
|
26
|
+
cli_send_email,
|
|
27
|
+
cli_send_notification,
|
|
28
|
+
)
|
|
29
|
+
from .constants import CLICK_CONTEXT_SETTINGS, TRACEBACK_SUMMARY_LIMIT, TRACEBACK_VERBOSE_LIMIT
|
|
30
|
+
from .context import (
|
|
31
|
+
TracebackState,
|
|
32
|
+
apply_traceback_preferences,
|
|
33
|
+
restore_traceback_state,
|
|
34
|
+
snapshot_traceback_state,
|
|
35
|
+
store_cli_context,
|
|
36
|
+
)
|
|
37
|
+
from .main import main
|
|
38
|
+
from .root import cli
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
# Constants
|
|
42
|
+
"CLICK_CONTEXT_SETTINGS",
|
|
43
|
+
"TRACEBACK_SUMMARY_LIMIT",
|
|
44
|
+
"TRACEBACK_VERBOSE_LIMIT",
|
|
45
|
+
# Traceback management
|
|
46
|
+
"TracebackState",
|
|
47
|
+
"apply_traceback_preferences",
|
|
48
|
+
# Root command
|
|
49
|
+
"cli",
|
|
50
|
+
# Commands
|
|
51
|
+
"cli_config",
|
|
52
|
+
"cli_config_deploy",
|
|
53
|
+
"cli_config_generate_examples",
|
|
54
|
+
"cli_fail",
|
|
55
|
+
"cli_hello",
|
|
56
|
+
"cli_info",
|
|
57
|
+
"cli_logdemo",
|
|
58
|
+
"cli_send_email",
|
|
59
|
+
"cli_send_notification",
|
|
60
|
+
# Entry point
|
|
61
|
+
"main",
|
|
62
|
+
"restore_traceback_state",
|
|
63
|
+
"snapshot_traceback_state",
|
|
64
|
+
# Context helpers
|
|
65
|
+
"store_cli_context",
|
|
66
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""CLI command implementations.
|
|
2
|
+
|
|
3
|
+
Collects all subcommand functions and re-exports them for registration
|
|
4
|
+
with the root CLI group.
|
|
5
|
+
|
|
6
|
+
Contents:
|
|
7
|
+
* Info commands from :mod:`.info`
|
|
8
|
+
* Config commands from :mod:`.config`
|
|
9
|
+
* Email commands from :mod:`.email` (subpackage)
|
|
10
|
+
* Logging commands from :mod:`.logging`
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from .config import cli_config, cli_config_deploy, cli_config_generate_examples
|
|
16
|
+
from .email import cli_send_email, cli_send_notification
|
|
17
|
+
from .info import cli_fail, cli_hello, cli_info
|
|
18
|
+
from .logging import cli_logdemo
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"cli_config",
|
|
22
|
+
"cli_config_deploy",
|
|
23
|
+
"cli_config_generate_examples",
|
|
24
|
+
"cli_fail",
|
|
25
|
+
"cli_hello",
|
|
26
|
+
"cli_info",
|
|
27
|
+
"cli_logdemo",
|
|
28
|
+
"cli_send_email",
|
|
29
|
+
"cli_send_notification",
|
|
30
|
+
]
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""Configuration display and deployment CLI commands.
|
|
2
|
+
|
|
3
|
+
Provides commands to inspect, deploy, and generate example configuration.
|
|
4
|
+
|
|
5
|
+
Contents:
|
|
6
|
+
* :func:`cli_config` - Display merged configuration.
|
|
7
|
+
* :func:`cli_config_deploy` - Deploy configuration to target locations.
|
|
8
|
+
* :func:`cli_config_generate_examples` - Generate example configuration files.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
import lib_log_rich.runtime
|
|
17
|
+
import rich_click as click
|
|
18
|
+
from lib_layered_config import Config, generate_examples
|
|
19
|
+
|
|
20
|
+
from agentdag import __init__conf__
|
|
21
|
+
from agentdag.adapters.config.overrides import apply_overrides
|
|
22
|
+
from agentdag.adapters.config.permissions import get_permission_defaults
|
|
23
|
+
from agentdag.domain.enums import DeployTarget, OutputFormat
|
|
24
|
+
|
|
25
|
+
from .. import safe_console
|
|
26
|
+
from ..constants import CLICK_CONTEXT_SETTINGS
|
|
27
|
+
from ..context import CLIContext, get_cli_context
|
|
28
|
+
from ..exit_codes import ExitCode
|
|
29
|
+
from ..typed_click import option
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@click.command("config", context_settings=CLICK_CONTEXT_SETTINGS)
|
|
38
|
+
@option(
|
|
39
|
+
"--format",
|
|
40
|
+
"output_format",
|
|
41
|
+
type=click.Choice([f.value for f in OutputFormat], case_sensitive=False),
|
|
42
|
+
default=OutputFormat.HUMAN.value,
|
|
43
|
+
help="Output format (human-readable or JSON)",
|
|
44
|
+
)
|
|
45
|
+
@option(
|
|
46
|
+
"--section",
|
|
47
|
+
type=str,
|
|
48
|
+
default=None,
|
|
49
|
+
help="Show only a specific configuration section (e.g., 'lib_log_rich')",
|
|
50
|
+
)
|
|
51
|
+
@option(
|
|
52
|
+
"--profile",
|
|
53
|
+
type=str,
|
|
54
|
+
default=None,
|
|
55
|
+
help="Override profile from root command (e.g., 'production', 'test')",
|
|
56
|
+
)
|
|
57
|
+
@click.pass_context
|
|
58
|
+
def cli_config(ctx: click.Context, output_format: str, section: str | None, profile: str | None) -> None:
|
|
59
|
+
"""Display the current merged configuration from all sources.
|
|
60
|
+
|
|
61
|
+
Shows configuration loaded from defaults, application/user config files,
|
|
62
|
+
.env files, and environment variables.
|
|
63
|
+
|
|
64
|
+
Precedence: defaults -> app -> host -> user -> dotenv -> env
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
>>> from click.testing import CliRunner
|
|
68
|
+
>>> from unittest.mock import MagicMock
|
|
69
|
+
>>> runner = CliRunner()
|
|
70
|
+
>>> # Real invocation tested in test_cli_config.py
|
|
71
|
+
"""
|
|
72
|
+
cli_ctx = get_cli_context(ctx)
|
|
73
|
+
effective_config, effective_profile = _resolve_config(cli_ctx, profile)
|
|
74
|
+
fmt = OutputFormat(output_format.lower())
|
|
75
|
+
|
|
76
|
+
extra = {"command": "config", "format": fmt.value, "profile": effective_profile}
|
|
77
|
+
with lib_log_rich.runtime.bind(job_id="cli-config", extra=extra):
|
|
78
|
+
logger.info(
|
|
79
|
+
"Displaying configuration",
|
|
80
|
+
extra={"format": fmt.value, "section": section, "profile": effective_profile},
|
|
81
|
+
)
|
|
82
|
+
safe_console.echo()
|
|
83
|
+
try:
|
|
84
|
+
cli_ctx.services.display_config(
|
|
85
|
+
effective_config, output_format=fmt, section=section, profile=effective_profile
|
|
86
|
+
)
|
|
87
|
+
except ValueError as exc:
|
|
88
|
+
safe_console.echo(f"\nError: {exc}", err=True)
|
|
89
|
+
raise SystemExit(ExitCode.INVALID_ARGUMENT) from exc
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _get_effective_profile(cli_ctx: CLIContext, profile_override: str | None) -> str | None:
|
|
93
|
+
"""Get effective profile: override takes precedence over context."""
|
|
94
|
+
return profile_override if profile_override else cli_ctx.profile
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _resolve_config(cli_ctx: CLIContext, profile: str | None) -> tuple[Config, str | None]:
|
|
98
|
+
"""Resolve configuration from context or reload with profile override.
|
|
99
|
+
|
|
100
|
+
When a subcommand-level profile override is specified, reloads config
|
|
101
|
+
with that profile and reapplies any root-level ``--set`` overrides
|
|
102
|
+
stored in the CLI context.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
cli_ctx: CLI context containing stored config and services.
|
|
106
|
+
profile: Optional profile override.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Tuple of (config, effective_profile).
|
|
110
|
+
"""
|
|
111
|
+
effective_profile = _get_effective_profile(cli_ctx, profile)
|
|
112
|
+
if profile:
|
|
113
|
+
config = cli_ctx.services.get_config(profile=profile)
|
|
114
|
+
return apply_overrides(config, cli_ctx.set_overrides), effective_profile
|
|
115
|
+
return cli_ctx.config, effective_profile
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _parse_octal_mode(ctx: click.Context, param: click.Parameter, value: str | None) -> int | None:
|
|
119
|
+
"""Parse octal mode string (e.g., '750' or '0o750') to int.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
ctx: Click context (unused but required by callback signature).
|
|
123
|
+
param: Click parameter (unused but required by callback signature).
|
|
124
|
+
value: Octal mode string from CLI, or None.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Integer permission mode, or None if value was None.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
click.BadParameter: If value cannot be parsed as octal.
|
|
131
|
+
"""
|
|
132
|
+
if value is None:
|
|
133
|
+
return None
|
|
134
|
+
try:
|
|
135
|
+
# Handle both '750' and '0o750' formats
|
|
136
|
+
return int(value, 8) if not value.startswith("0o") else int(value, 0)
|
|
137
|
+
except ValueError as exc:
|
|
138
|
+
raise click.BadParameter(f"Invalid octal mode: {value}") from exc
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@click.command("config-deploy", context_settings=CLICK_CONTEXT_SETTINGS)
|
|
142
|
+
@option(
|
|
143
|
+
"--target",
|
|
144
|
+
"targets",
|
|
145
|
+
type=click.Choice([t.value for t in DeployTarget], case_sensitive=False),
|
|
146
|
+
multiple=True,
|
|
147
|
+
required=True,
|
|
148
|
+
help="Target configuration layer(s) to deploy to (can specify multiple)",
|
|
149
|
+
)
|
|
150
|
+
@option(
|
|
151
|
+
"--force",
|
|
152
|
+
is_flag=True,
|
|
153
|
+
default=False,
|
|
154
|
+
help="Overwrite existing configuration files",
|
|
155
|
+
)
|
|
156
|
+
@option(
|
|
157
|
+
"--profile",
|
|
158
|
+
type=str,
|
|
159
|
+
default=None,
|
|
160
|
+
help="Override profile from root command (e.g., 'production', 'test')",
|
|
161
|
+
)
|
|
162
|
+
@option(
|
|
163
|
+
"--permissions/--no-permissions",
|
|
164
|
+
"set_permissions",
|
|
165
|
+
default=None,
|
|
166
|
+
help="Set Unix permissions (755/644 for app/host, 700/600 for user). Default: enabled.",
|
|
167
|
+
)
|
|
168
|
+
@option(
|
|
169
|
+
"--dir-mode",
|
|
170
|
+
type=str,
|
|
171
|
+
default=None,
|
|
172
|
+
callback=_parse_octal_mode,
|
|
173
|
+
help="Override directory mode (octal, e.g., 750 or 0o750)",
|
|
174
|
+
)
|
|
175
|
+
@option(
|
|
176
|
+
"--file-mode",
|
|
177
|
+
type=str,
|
|
178
|
+
default=None,
|
|
179
|
+
callback=_parse_octal_mode,
|
|
180
|
+
help="Override file mode (octal, e.g., 640 or 0o640)",
|
|
181
|
+
)
|
|
182
|
+
@click.pass_context
|
|
183
|
+
def cli_config_deploy(
|
|
184
|
+
ctx: click.Context,
|
|
185
|
+
*,
|
|
186
|
+
targets: tuple[str, ...],
|
|
187
|
+
force: bool,
|
|
188
|
+
profile: str | None,
|
|
189
|
+
set_permissions: bool | None,
|
|
190
|
+
dir_mode: int | None,
|
|
191
|
+
file_mode: int | None,
|
|
192
|
+
) -> None:
|
|
193
|
+
r"""Deploy default configuration to system or user directories.
|
|
194
|
+
|
|
195
|
+
Creates configuration files in platform-specific locations:
|
|
196
|
+
|
|
197
|
+
\b
|
|
198
|
+
- app: System-wide application config (requires privileges)
|
|
199
|
+
- host: System-wide host config (requires privileges)
|
|
200
|
+
- user: User-specific config (~/.config on Linux)
|
|
201
|
+
|
|
202
|
+
By default, existing files are not overwritten. Use --force to overwrite.
|
|
203
|
+
|
|
204
|
+
\b
|
|
205
|
+
Permission options (POSIX only, no-op on Windows):
|
|
206
|
+
- --permissions/--no-permissions: Enable/disable permission setting
|
|
207
|
+
- --dir-mode: Override directory mode (octal, e.g., 750)
|
|
208
|
+
- --file-mode: Override file mode (octal, e.g., 640)
|
|
209
|
+
|
|
210
|
+
Example:
|
|
211
|
+
>>> from click.testing import CliRunner
|
|
212
|
+
>>> runner = CliRunner()
|
|
213
|
+
>>> # Real invocation tested in test_cli_config.py
|
|
214
|
+
"""
|
|
215
|
+
cli_ctx = get_cli_context(ctx)
|
|
216
|
+
effective_profile = _get_effective_profile(cli_ctx, profile)
|
|
217
|
+
deploy_targets = tuple(DeployTarget(t.lower()) for t in targets)
|
|
218
|
+
target_values = tuple(t.value for t in deploy_targets)
|
|
219
|
+
|
|
220
|
+
extra = {"command": "config-deploy", "targets": target_values, "force": force, "profile": effective_profile}
|
|
221
|
+
with lib_log_rich.runtime.bind(job_id="cli-config-deploy", extra=extra):
|
|
222
|
+
logger.info(
|
|
223
|
+
"Deploying configuration",
|
|
224
|
+
extra={"targets": target_values, "force": force, "profile": effective_profile},
|
|
225
|
+
)
|
|
226
|
+
_execute_deploy(
|
|
227
|
+
cli_ctx,
|
|
228
|
+
targets=deploy_targets,
|
|
229
|
+
force=force,
|
|
230
|
+
profile=effective_profile,
|
|
231
|
+
set_permissions=set_permissions,
|
|
232
|
+
dir_mode=dir_mode,
|
|
233
|
+
file_mode=file_mode,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _execute_deploy(
|
|
238
|
+
cli_ctx: CLIContext,
|
|
239
|
+
*,
|
|
240
|
+
targets: tuple[DeployTarget, ...],
|
|
241
|
+
force: bool,
|
|
242
|
+
profile: str | None,
|
|
243
|
+
set_permissions: bool | None,
|
|
244
|
+
dir_mode: int | None,
|
|
245
|
+
file_mode: int | None,
|
|
246
|
+
) -> None:
|
|
247
|
+
"""Execute configuration deployment with error handling.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
cli_ctx: CLI context containing services.
|
|
251
|
+
targets: Deployment target layers.
|
|
252
|
+
force: Whether to overwrite existing files.
|
|
253
|
+
profile: Optional profile name.
|
|
254
|
+
set_permissions: Whether to set Unix permissions. None uses config default.
|
|
255
|
+
dir_mode: Override directory permission mode.
|
|
256
|
+
file_mode: Override file permission mode.
|
|
257
|
+
|
|
258
|
+
Raises:
|
|
259
|
+
SystemExit: On permission or other errors.
|
|
260
|
+
"""
|
|
261
|
+
# Get permission defaults from config
|
|
262
|
+
perm_defaults = get_permission_defaults(cli_ctx.config)
|
|
263
|
+
|
|
264
|
+
# CLI --permissions/--no-permissions overrides config enabled setting
|
|
265
|
+
effective_set_permissions = set_permissions if set_permissions is not None else perm_defaults.enabled
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
deployed_paths = cli_ctx.services.deploy_configuration(
|
|
269
|
+
targets=targets,
|
|
270
|
+
force=force,
|
|
271
|
+
profile=profile,
|
|
272
|
+
set_permissions=effective_set_permissions,
|
|
273
|
+
dir_mode=dir_mode,
|
|
274
|
+
file_mode=file_mode,
|
|
275
|
+
)
|
|
276
|
+
_report_deployment_result(deployed_paths, profile, effective_set_permissions)
|
|
277
|
+
except PermissionError as exc:
|
|
278
|
+
logger.error("Permission denied when deploying configuration", extra={"error": str(exc)})
|
|
279
|
+
safe_console.echo(f"\nError: Permission denied. {exc}", err=True)
|
|
280
|
+
safe_console.echo("Hint: System-wide deployment (--target app/host) may require sudo.", err=True)
|
|
281
|
+
raise SystemExit(ExitCode.PERMISSION_DENIED) from exc
|
|
282
|
+
except Exception as exc:
|
|
283
|
+
logger.error("Failed to deploy configuration", extra={"error": str(exc), "error_type": type(exc).__name__})
|
|
284
|
+
safe_console.echo(f"\nError: Failed to deploy configuration: {exc}", err=True)
|
|
285
|
+
raise SystemExit(ExitCode.GENERAL_ERROR) from exc
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _report_deployment_result(deployed_paths: list[Path], profile: str | None, set_permissions: bool) -> None:
|
|
289
|
+
"""Report deployment results to the user.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
deployed_paths: List of paths where configs were deployed.
|
|
293
|
+
profile: Optional profile name for display.
|
|
294
|
+
set_permissions: Whether permissions were set.
|
|
295
|
+
"""
|
|
296
|
+
if deployed_paths:
|
|
297
|
+
profile_msg = f" (profile: {profile})" if profile else ""
|
|
298
|
+
perm_msg = "" if set_permissions else " (permissions not set)"
|
|
299
|
+
safe_console.echo(f"\nConfiguration deployed successfully{profile_msg}{perm_msg}:")
|
|
300
|
+
for path in deployed_paths:
|
|
301
|
+
# ASCII marker on purpose: a non-ASCII glyph here crashes config-deploy with a
|
|
302
|
+
# UnicodeEncodeError on a legacy Windows console codepage (cp1252) even though the
|
|
303
|
+
# files were already written, so exit 1 misreports a deploy that actually succeeded.
|
|
304
|
+
safe_console.echo(f" + {path}")
|
|
305
|
+
else:
|
|
306
|
+
safe_console.echo("\nNo files were created (all target files already exist).")
|
|
307
|
+
safe_console.echo("Use --force to overwrite existing configuration files.")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@click.command("config-generate-examples", context_settings=CLICK_CONTEXT_SETTINGS)
|
|
311
|
+
@option("--destination", type=click.Path(file_okay=False), required=True, help="Directory to write example files")
|
|
312
|
+
@option("--force", is_flag=True, default=False, help="Overwrite existing files")
|
|
313
|
+
@click.pass_context
|
|
314
|
+
def cli_config_generate_examples(ctx: click.Context, destination: str, force: bool) -> None:
|
|
315
|
+
"""Generate example configuration files in a target directory.
|
|
316
|
+
|
|
317
|
+
Creates example TOML configuration files showing all available options
|
|
318
|
+
with their default values and documentation comments. Useful for learning
|
|
319
|
+
the configuration structure, creating initial configuration files, or
|
|
320
|
+
documenting available settings.
|
|
321
|
+
|
|
322
|
+
By default, existing files are not overwritten. Use --force to overwrite.
|
|
323
|
+
|
|
324
|
+
Example:
|
|
325
|
+
>>> from click.testing import CliRunner
|
|
326
|
+
>>> runner = CliRunner()
|
|
327
|
+
>>> # Real invocation tested in test_cli_config.py
|
|
328
|
+
"""
|
|
329
|
+
extra = {"command": "config-generate-examples", "destination": destination, "force": force}
|
|
330
|
+
with lib_log_rich.runtime.bind(job_id="cli-config-generate-examples", extra=extra):
|
|
331
|
+
logger.info("Generating example configuration files", extra={"destination": destination, "force": force})
|
|
332
|
+
try:
|
|
333
|
+
paths = generate_examples(
|
|
334
|
+
destination=destination,
|
|
335
|
+
slug=__init__conf__.LAYEREDCONF_SLUG,
|
|
336
|
+
vendor=__init__conf__.LAYEREDCONF_VENDOR,
|
|
337
|
+
app=__init__conf__.LAYEREDCONF_APP,
|
|
338
|
+
force=force,
|
|
339
|
+
)
|
|
340
|
+
if paths:
|
|
341
|
+
safe_console.echo(f"\nGenerated {len(paths)} example file(s):")
|
|
342
|
+
for p in paths:
|
|
343
|
+
safe_console.echo(f" {p}")
|
|
344
|
+
else:
|
|
345
|
+
safe_console.echo("\nNo files generated (all already exist). Use --force to overwrite.")
|
|
346
|
+
except Exception as exc:
|
|
347
|
+
logger.error("Failed to generate examples", extra={"error": str(exc)})
|
|
348
|
+
safe_console.echo(f"\nError: {exc}", err=True)
|
|
349
|
+
raise SystemExit(ExitCode.GENERAL_ERROR) from exc
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
__all__ = ["cli_config", "cli_config_deploy", "cli_config_generate_examples"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Email sending CLI commands.
|
|
2
|
+
|
|
3
|
+
Provides commands for sending emails and notifications via SMTP.
|
|
4
|
+
|
|
5
|
+
Contents:
|
|
6
|
+
* :func:`.send_email.cli_send_email` - Send email with optional HTML and attachments.
|
|
7
|
+
* :func:`.send_notification.cli_send_notification` - Send simple plain-text notification.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from ._common import filter_sentinels
|
|
13
|
+
from .send_email import cli_send_email
|
|
14
|
+
from .send_notification import cli_send_notification
|
|
15
|
+
|
|
16
|
+
__all__ = ["cli_send_email", "cli_send_notification", "filter_sentinels"]
|