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.
Files changed (63) hide show
  1. agentdag/__init__.py +29 -0
  2. agentdag/__init__conf__.py +90 -0
  3. agentdag/__main__.py +9 -0
  4. agentdag/adapters/__init__.py +15 -0
  5. agentdag/adapters/cli/__init__.py +66 -0
  6. agentdag/adapters/cli/commands/__init__.py +30 -0
  7. agentdag/adapters/cli/commands/config.py +352 -0
  8. agentdag/adapters/cli/commands/email/__init__.py +16 -0
  9. agentdag/adapters/cli/commands/email/_common.py +288 -0
  10. agentdag/adapters/cli/commands/email/send_email.py +140 -0
  11. agentdag/adapters/cli/commands/email/send_notification.py +99 -0
  12. agentdag/adapters/cli/commands/info.py +75 -0
  13. agentdag/adapters/cli/commands/logging.py +34 -0
  14. agentdag/adapters/cli/constants.py +28 -0
  15. agentdag/adapters/cli/context.py +156 -0
  16. agentdag/adapters/cli/exit_codes.py +52 -0
  17. agentdag/adapters/cli/main.py +119 -0
  18. agentdag/adapters/cli/py.typed +0 -0
  19. agentdag/adapters/cli/root.py +165 -0
  20. agentdag/adapters/cli/safe_console.py +213 -0
  21. agentdag/adapters/cli/typed_click.py +51 -0
  22. agentdag/adapters/config/__init__.py +25 -0
  23. agentdag/adapters/config/defaultconfig.d/40-layered-config.toml +71 -0
  24. agentdag/adapters/config/defaultconfig.d/50-mail.toml +222 -0
  25. agentdag/adapters/config/defaultconfig.d/90-logging.toml +432 -0
  26. agentdag/adapters/config/defaultconfig.toml +46 -0
  27. agentdag/adapters/config/deploy.py +122 -0
  28. agentdag/adapters/config/display.py +60 -0
  29. agentdag/adapters/config/loader.py +198 -0
  30. agentdag/adapters/config/overrides.py +191 -0
  31. agentdag/adapters/config/permissions.py +186 -0
  32. agentdag/adapters/config/py.typed +0 -0
  33. agentdag/adapters/email/__init__.py +28 -0
  34. agentdag/adapters/email/config.py +311 -0
  35. agentdag/adapters/email/py.typed +0 -0
  36. agentdag/adapters/email/sender.py +26 -0
  37. agentdag/adapters/email/transport.py +287 -0
  38. agentdag/adapters/email/validation.py +65 -0
  39. agentdag/adapters/logging/__init__.py +13 -0
  40. agentdag/adapters/logging/py.typed +0 -0
  41. agentdag/adapters/logging/setup.py +134 -0
  42. agentdag/adapters/memory/__init__.py +50 -0
  43. agentdag/adapters/memory/config.py +65 -0
  44. agentdag/adapters/memory/email.py +181 -0
  45. agentdag/adapters/memory/logging.py +18 -0
  46. agentdag/adapters/py.typed +0 -0
  47. agentdag/application/__init__.py +32 -0
  48. agentdag/application/ports.py +115 -0
  49. agentdag/application/py.typed +0 -0
  50. agentdag/composition/__init__.py +128 -0
  51. agentdag/composition/py.typed +0 -0
  52. agentdag/domain/__init__.py +32 -0
  53. agentdag/domain/behaviors.py +28 -0
  54. agentdag/domain/enums.py +55 -0
  55. agentdag/domain/errors.py +56 -0
  56. agentdag/domain/py.typed +0 -0
  57. agentdag/entry.py +26 -0
  58. agentdag/py.typed +0 -0
  59. agentdag-0.0.1.dist-info/METADATA +430 -0
  60. agentdag-0.0.1.dist-info/RECORD +63 -0
  61. agentdag-0.0.1.dist-info/WHEEL +4 -0
  62. agentdag-0.0.1.dist-info/entry_points.txt +2 -0
  63. 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"]