Skip to content

Commit 1ac2a9a

Browse files
committed
Address PR review feedback for DynamicD
- Restructure imports to avoid impacting ddev startup time: - Create lightweight dynamicd.py command entry point with lazy imports - Rename dynamicd/ folder to _dynamicd/ (internal utilities) - Empty _dynamicd/__init__.py to prevent import tree pollution - Remove redundant pyyaml dependency (transitive from datadog-checks-dev) - Add README documentation link to --help output - Use app.display_header() instead of hardcoded ASCII formatting - Extract helper functions for better maintainability: - _get_api_keys() for API key validation - _validate_and_warn_internal_org() for org validation - Make requests import lazy inside _validate_org()
1 parent 5bc14c6 commit 1ac2a9a

File tree

12 files changed

+209
-166
lines changed

12 files changed

+209
-166
lines changed

ddev/pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ dependencies = [
3535
"httpx",
3636
"jsonpointer",
3737
"pluggy",
38-
"pyyaml",
3938
"rich>=12.5.1",
4039
"stamina==23.2.0",
4140
"tomli; python_version < '3.11'",
File renamed without changes.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# (C) Datadog, Inc. 2024-present
2+
# All rights reserved
3+
# Licensed under a 3-clause BSD style license (see LICENSE)
4+
"""DynamicD utilities - Smart fake data generator for Datadog integrations."""

ddev/src/ddev/cli/meta/scripts/dynamicd/cli.py renamed to ddev/src/ddev/cli/meta/scripts/_dynamicd/cli.py

Lines changed: 73 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,30 @@
11
# (C) Datadog, Inc. 2024-present
22
# All rights reserved
33
# Licensed under a 3-clause BSD style license (see LICENSE)
4-
"""CLI interface for DynamicD."""
4+
"""CLI implementation for DynamicD."""
55

66
from __future__ import annotations
77

88
import os
99
from typing import TYPE_CHECKING
1010

1111
import click
12-
import requests
1312

14-
from ddev.cli.meta.scripts.dynamicd.constants import (
15-
DEFAULT_DURATION_SECONDS,
16-
DEFAULT_METRICS_PER_BATCH,
17-
SCENARIOS,
18-
)
13+
from ddev.cli.meta.scripts._dynamicd.constants import SCENARIOS
1914

2015
if TYPE_CHECKING:
2116
from ddev.cli.application import Application
2217

2318

24-
def validate_org(api_key: str, app_key: str | None, site: str) -> tuple[bool, str, bool]:
19+
def _validate_org(api_key: str, app_key: str | None, site: str) -> tuple[bool, str, bool]:
2520
"""Validate API key and return (is_internal_org, org_name, key_valid).
2621
2722
Checks if the API key belongs to a Datadog internal org (HQ or Staging).
2823
Note: Org lookup requires an Application Key. If not provided, we can only
2924
validate the API key works but cannot determine the org name.
3025
"""
26+
import requests
27+
3128
# First validate the API key
3229
try:
3330
resp = requests.get(
@@ -76,118 +73,11 @@ def validate_org(api_key: str, app_key: str | None, site: str) -> tuple[bool, st
7673
return False, f"(unexpected error: {type(e).__name__}: {e})", True
7774

7875

79-
@click.command("dynamicd", short_help="Generate realistic fake telemetry data using AI")
80-
@click.argument("integration")
81-
@click.option(
82-
"--scenario",
83-
"-s",
84-
type=click.Choice(list(SCENARIOS.keys())),
85-
default=None,
86-
help="Scenario to simulate. If not provided, shows interactive menu.",
87-
)
88-
@click.option(
89-
"--duration",
90-
"-d",
91-
type=int,
92-
default=DEFAULT_DURATION_SECONDS,
93-
help="Duration in seconds. Default: 0 (run forever until Ctrl+C)",
94-
)
95-
@click.option(
96-
"--rate",
97-
"-r",
98-
type=int,
99-
default=DEFAULT_METRICS_PER_BATCH,
100-
help=f"Target metrics per batch (batches sent every 10s). Default: {DEFAULT_METRICS_PER_BATCH}",
101-
)
102-
@click.option(
103-
"--save",
104-
is_flag=True,
105-
help="Save the generated script to the integration's fake_data/ directory",
106-
)
107-
@click.option(
108-
"--show-only",
109-
is_flag=True,
110-
help="Only show the generated script, don't execute it",
111-
)
112-
@click.option(
113-
"--timeout",
114-
type=int,
115-
default=None,
116-
help="Execution timeout in seconds (for testing). Default: no timeout",
117-
)
118-
@click.option(
119-
"--all-metrics",
120-
is_flag=True,
121-
help="Generate ALL metrics from metadata.csv, not just dashboard metrics. Use for load testing.",
122-
)
123-
@click.option(
124-
"--sandbox/--no-sandbox",
125-
default=True,
126-
help="Run script in Docker container for isolation (default: enabled). Use --no-sandbox to run directly.",
127-
)
128-
@click.pass_obj
129-
def dynamicd(
130-
app: Application,
131-
integration: str,
132-
scenario: str | None,
133-
duration: int,
134-
rate: int,
135-
save: bool,
136-
show_only: bool,
137-
timeout: int | None,
138-
all_metrics: bool,
139-
sandbox: bool,
140-
):
141-
"""Generate realistic fake telemetry data for an integration using AI.
76+
def _get_api_keys(app: Application) -> tuple[str, str]:
77+
"""Get and validate API keys from config/environment.
14278
143-
DynamicD uses Claude to analyze your integration's metrics and generate
144-
a sophisticated simulator that produces realistic, scenario-aware data.
145-
146-
\b
147-
Examples:
148-
# Interactive scenario selection
149-
ddev meta scripts dynamicd ibm_mq
150-
151-
# Specific scenario
152-
ddev meta scripts dynamicd ibm_mq --scenario incident
153-
154-
# Save the script for later use
155-
ddev meta scripts dynamicd ibm_mq --scenario healthy --save
156-
157-
# Just show the generated script
158-
ddev meta scripts dynamicd ibm_mq --show-only
79+
Returns (llm_api_key, dd_api_key) or aborts if not configured.
15980
"""
160-
from ddev.cli.meta.scripts.dynamicd.context_builder import build_context
161-
from ddev.cli.meta.scripts.dynamicd.executor import (
162-
execute_script,
163-
is_docker_available,
164-
save_script,
165-
validate_script_syntax,
166-
)
167-
from ddev.cli.meta.scripts.dynamicd.generator import GeneratorError, generate_simulator_script
168-
169-
# Get the integration
170-
try:
171-
intg = app.repo.integrations.get(integration)
172-
except OSError:
173-
app.abort(f"Unknown integration: {integration}")
174-
175-
# Check for metrics
176-
if not intg.has_metrics:
177-
app.abort(f"Integration '{integration}' has no metrics defined in metadata.csv")
178-
179-
# Validate numeric options
180-
if duration < 0:
181-
app.abort("Duration cannot be negative")
182-
if rate <= 0:
183-
app.abort("Rate must be a positive number")
184-
185-
# Handle sandbox mode (default: enabled)
186-
use_sandbox = sandbox
187-
if use_sandbox and not is_docker_available():
188-
app.display_error("Docker is not available. Install Docker or use --no-sandbox.")
189-
app.abort()
190-
19181
# Get LLM API key from config or environment variable
19282
llm_api_key = app.config.raw_data.get("dynamicd", {}).get("llm_api_key")
19383
if not llm_api_key:
@@ -208,13 +98,16 @@ def dynamicd(
20898
)
20999
app.abort()
210100

211-
# Get Datadog site and app key
212-
dd_site = app.config.org.config.get("site", "datadoghq.com")
213-
dd_app_key = app.config.org.config.get("app_key")
101+
return llm_api_key, dd_api_key
102+
214103

215-
# Validate org and warn if internal Datadog org
104+
def _validate_and_warn_internal_org(app: Application, dd_api_key: str, dd_app_key: str | None, dd_site: str) -> None:
105+
"""Validate Datadog org and warn if it's an internal Datadog org.
106+
107+
Aborts if the API key is invalid or user declines to continue for internal orgs.
108+
"""
216109
app.display_info("Validating Datadog API key...")
217-
is_internal_org, org_name, key_valid = validate_org(dd_api_key, dd_app_key, dd_site)
110+
is_internal_org, org_name, key_valid = _validate_org(dd_api_key, dd_app_key, dd_site)
218111

219112
if not key_valid:
220113
app.display_error(f"API key validation failed: {org_name}")
@@ -225,11 +118,10 @@ def dynamicd(
225118
app.display_info("")
226119

227120
if is_internal_org:
228-
app.display_warning("=" * 60)
229-
app.display_warning("WARNING: You are about to send fake data to a Datadog internal org!")
121+
app.display_header("WARNING", line_style="bold yellow")
122+
app.display_warning("You are about to send fake data to a Datadog internal org!")
230123
app.display_warning(f" Org: {org_name}")
231124
app.display_warning(f" Site: {dd_site}")
232-
app.display_warning("=" * 60)
233125
app.display_warning("")
234126
confirm = click.prompt(
235127
"Are you sure you want to continue? Type 'y' to proceed",
@@ -240,6 +132,58 @@ def dynamicd(
240132
app.display_info("Aborted by user.")
241133
app.abort()
242134

135+
136+
def run_dynamicd(
137+
app: Application,
138+
integration: str,
139+
scenario: str | None,
140+
duration: int,
141+
rate: int,
142+
save: bool,
143+
show_only: bool,
144+
timeout: int | None,
145+
all_metrics: bool,
146+
sandbox: bool,
147+
) -> None:
148+
"""Run the DynamicD command logic."""
149+
from ddev.cli.meta.scripts._dynamicd.context_builder import build_context
150+
from ddev.cli.meta.scripts._dynamicd.executor import (
151+
execute_script,
152+
is_docker_available,
153+
save_script,
154+
validate_script_syntax,
155+
)
156+
from ddev.cli.meta.scripts._dynamicd.generator import GeneratorError, generate_simulator_script
157+
158+
# Validate integration
159+
try:
160+
intg = app.repo.integrations.get(integration)
161+
except OSError:
162+
app.abort(f"Unknown integration: {integration}")
163+
164+
if not intg.has_metrics:
165+
app.abort(f"Integration '{integration}' has no metrics defined in metadata.csv")
166+
167+
# Validate options
168+
if duration < 0:
169+
app.abort("Duration cannot be negative")
170+
if rate <= 0:
171+
app.abort("Rate must be a positive number")
172+
173+
# Validate sandbox availability
174+
use_sandbox = sandbox
175+
if use_sandbox and not is_docker_available():
176+
app.display_error("Docker is not available. Install Docker or use --no-sandbox.")
177+
app.abort()
178+
179+
# Get and validate API keys
180+
llm_api_key, dd_api_key = _get_api_keys(app)
181+
dd_site = app.config.org.config.get("site", "datadoghq.com")
182+
dd_app_key = app.config.org.config.get("app_key")
183+
184+
# Validate org and warn if internal
185+
_validate_and_warn_internal_org(app, dd_api_key, dd_app_key, dd_site)
186+
243187
# Interactive scenario selection if not provided
244188
if scenario is None:
245189
scenario = _select_scenario_interactive(app)
@@ -249,10 +193,7 @@ def dynamicd(
249193
# Type narrowing: scenario is guaranteed to be str after the above check
250194
assert scenario is not None
251195

252-
app.display_info("")
253-
app.display_info(f"╔{'═' * 60}╗")
254-
app.display_info(f"║{'DynamicD - Smart Fake Data Generator':^60}║")
255-
app.display_info(f"╚{'═' * 60}╝")
196+
app.display_header("DynamicD - Smart Fake Data Generator")
256197
app.display_info("")
257198
app.display_info(f" Integration: {intg.display_name}")
258199
app.display_info(f" Scenario: {scenario}")
@@ -300,12 +241,9 @@ def on_status(msg: str) -> None:
300241

301242
# Show only mode
302243
if show_only:
303-
app.display_info("")
304-
app.display_info("=" * 70)
305-
app.display_info("GENERATED SCRIPT")
306-
app.display_info("=" * 70)
244+
app.display_header("Generated Script")
307245
click.echo(script)
308-
app.display_info("=" * 70)
246+
app.display_header("")
309247
return
310248

311249
# Save if requested
File renamed without changes.

ddev/src/ddev/cli/meta/scripts/dynamicd/context_builder.py renamed to ddev/src/ddev/cli/meta/scripts/_dynamicd/context_builder.py

File renamed without changes.

ddev/src/ddev/cli/meta/scripts/dynamicd/executor.py renamed to ddev/src/ddev/cli/meta/scripts/_dynamicd/executor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@
1414
from dataclasses import dataclass
1515
from pathlib import Path
1616

17-
from ddev.cli.meta.scripts.dynamicd.constants import (
17+
from ddev.cli.meta.scripts._dynamicd.constants import (
1818
DOCKER_CPU_LIMIT,
1919
DOCKER_IMAGE,
2020
DOCKER_MEMORY_LIMIT,
2121
FAKE_DATA_DIR,
2222
MAX_RETRIES,
2323
)
24-
from ddev.cli.meta.scripts.dynamicd.generator import GeneratorError, fix_script_error
24+
from ddev.cli.meta.scripts._dynamicd.generator import GeneratorError, fix_script_error
2525

2626

2727
@dataclass

ddev/src/ddev/cli/meta/scripts/dynamicd/generator.py renamed to ddev/src/ddev/cli/meta/scripts/_dynamicd/generator.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@
99
from collections.abc import Callable
1010
from typing import TYPE_CHECKING
1111

12-
from ddev.cli.meta.scripts.dynamicd.constants import DEFAULT_MODEL, MAX_TOKENS
13-
from ddev.cli.meta.scripts.dynamicd.prompts import (
12+
from ddev.cli.meta.scripts._dynamicd.constants import DEFAULT_MODEL, MAX_TOKENS
13+
from ddev.cli.meta.scripts._dynamicd.prompts import (
1414
build_error_correction_prompt,
1515
build_stage1_prompt,
1616
build_stage2_prompt,
1717
)
1818

1919
if TYPE_CHECKING:
20-
from ddev.cli.meta.scripts.dynamicd.context_builder import IntegrationContext
20+
from ddev.cli.meta.scripts._dynamicd.context_builder import IntegrationContext
2121

2222

2323
class GeneratorError(Exception):

ddev/src/ddev/cli/meta/scripts/dynamicd/prompts.py renamed to ddev/src/ddev/cli/meta/scripts/_dynamicd/prompts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from __future__ import annotations
77

8-
from ddev.cli.meta.scripts.dynamicd.constants import SCENARIOS
8+
from ddev.cli.meta.scripts._dynamicd.constants import SCENARIOS
99

1010
# =============================================================================
1111
# STAGE 1: Context Analysis Prompt

0 commit comments

Comments
 (0)