detectkit 0.1.0__tar.gz → 0.1.2__tar.gz

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 (60) hide show
  1. {detectkit-0.1.0/detectkit.egg-info → detectkit-0.1.2}/PKG-INFO +14 -9
  2. {detectkit-0.1.0 → detectkit-0.1.2}/README.md +13 -8
  3. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/cli/commands/run.py +63 -40
  4. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/config/metric_config.py +32 -0
  5. detectkit-0.1.2/detectkit/config/validator.py +124 -0
  6. detectkit-0.1.2/detectkit/detectors/statistical/iqr.py +418 -0
  7. detectkit-0.1.2/detectkit/detectors/statistical/zscore.py +404 -0
  8. {detectkit-0.1.0 → detectkit-0.1.2/detectkit.egg-info}/PKG-INFO +14 -9
  9. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit.egg-info/SOURCES.txt +1 -0
  10. {detectkit-0.1.0 → detectkit-0.1.2}/pyproject.toml +1 -1
  11. detectkit-0.1.0/detectkit/detectors/statistical/iqr.py +0 -230
  12. detectkit-0.1.0/detectkit/detectors/statistical/zscore.py +0 -225
  13. {detectkit-0.1.0 → detectkit-0.1.2}/LICENSE +0 -0
  14. {detectkit-0.1.0 → detectkit-0.1.2}/MANIFEST.in +0 -0
  15. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/__init__.py +0 -0
  16. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/alerting/__init__.py +0 -0
  17. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/alerting/channels/__init__.py +0 -0
  18. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/alerting/channels/base.py +0 -0
  19. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/alerting/channels/email.py +0 -0
  20. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/alerting/channels/factory.py +0 -0
  21. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/alerting/channels/mattermost.py +0 -0
  22. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/alerting/channels/slack.py +0 -0
  23. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/alerting/channels/telegram.py +0 -0
  24. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/alerting/channels/webhook.py +0 -0
  25. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/alerting/orchestrator.py +0 -0
  26. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/cli/__init__.py +0 -0
  27. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/cli/commands/__init__.py +0 -0
  28. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/cli/commands/init.py +0 -0
  29. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/cli/commands/test_alert.py +0 -0
  30. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/cli/main.py +0 -0
  31. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/config/__init__.py +0 -0
  32. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/config/profile.py +0 -0
  33. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/config/project_config.py +0 -0
  34. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/core/__init__.py +0 -0
  35. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/core/interval.py +0 -0
  36. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/core/models.py +0 -0
  37. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/database/__init__.py +0 -0
  38. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/database/clickhouse_manager.py +0 -0
  39. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/database/internal_tables.py +0 -0
  40. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/database/manager.py +0 -0
  41. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/database/tables.py +0 -0
  42. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/detectors/__init__.py +0 -0
  43. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/detectors/base.py +0 -0
  44. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/detectors/factory.py +0 -0
  45. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/detectors/statistical/__init__.py +0 -0
  46. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/detectors/statistical/mad.py +0 -0
  47. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  48. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/loaders/__init__.py +0 -0
  49. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/loaders/metric_loader.py +0 -0
  50. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/loaders/query_template.py +0 -0
  51. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/orchestration/__init__.py +0 -0
  52. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/orchestration/task_manager.py +0 -0
  53. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit/utils/__init__.py +0 -0
  54. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit.egg-info/dependency_links.txt +0 -0
  55. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit.egg-info/entry_points.txt +0 -0
  56. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit.egg-info/requires.txt +0 -0
  57. {detectkit-0.1.0 → detectkit-0.1.2}/detectkit.egg-info/top_level.txt +0 -0
  58. {detectkit-0.1.0 → detectkit-0.1.2}/requirements.txt +0 -0
  59. {detectkit-0.1.0 → detectkit-0.1.2}/setup.cfg +0 -0
  60. {detectkit-0.1.0 → detectkit-0.1.2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
@@ -67,9 +67,11 @@ Dynamic: license-file
67
67
 
68
68
  ## Status
69
69
 
70
- 🚧 **In Active Development** - Version 0.1.0
70
+ **Production Ready** - Version 0.1.2
71
71
 
72
- This is a complete rewrite of the original detectk library with modern architecture and best practices (2025).
72
+ Published to PyPI: https://pypi.org/project/detectkit/
73
+
74
+ Complete rewrite with modern architecture and full documentation (2025).
73
75
 
74
76
  ## Features
75
77
 
@@ -191,14 +193,17 @@ pytest tests/ --cov=detectkit --cov-report=html
191
193
  - ⚠️ Advanced detectors (Prophet, TimesFM) - optional extras
192
194
  - ⚠️ Additional alert channels (Telegram, Email) - optional
193
195
 
194
- See [TODO.md](TODO.md) for detailed development roadmap.
195
-
196
196
  ## Documentation
197
197
 
198
- - [ARCHITECTURE.md](ARCHITECTURE.md) - System architecture and design
199
- - [TECHNICAL_SPEC.md](TECHNICAL_SPEC.md) - Complete technical specification (Russian)
200
- - [TODO.md](TODO.md) - Development roadmap
201
- - [CLAUDE.md](CLAUDE.md) - Development context for AI assistants
198
+ 📚 **Complete documentation available at: https://github.com/alexeiveselov92/detectkit/tree/main/docs**
199
+
200
+ - [Getting Started](https://github.com/alexeiveselov92/detectkit/blob/main/docs/getting-started/quickstart.md) - 5-minute quickstart
201
+ - [Configuration Guide](https://github.com/alexeiveselov92/detectkit/blob/main/docs/guides/configuration.md) - All configuration options
202
+ - [Detectors Guide](https://github.com/alexeiveselov92/detectkit/blob/main/docs/guides/detectors.md) - Choosing the right detector
203
+ - [Alerting Guide](https://github.com/alexeiveselov92/detectkit/blob/main/docs/guides/alerting.md) - Setting up alerts
204
+ - [CLI Reference](https://github.com/alexeiveselov92/detectkit/blob/main/docs/reference/cli.md) - Command-line documentation
205
+ - [Examples](https://github.com/alexeiveselov92/detectkit/tree/main/docs/examples) - Real-world monitoring scenarios
206
+
202
207
 
203
208
  ## Requirements
204
209
 
@@ -6,9 +6,11 @@
6
6
 
7
7
  ## Status
8
8
 
9
- 🚧 **In Active Development** - Version 0.1.0
9
+ **Production Ready** - Version 0.1.2
10
10
 
11
- This is a complete rewrite of the original detectk library with modern architecture and best practices (2025).
11
+ Published to PyPI: https://pypi.org/project/detectkit/
12
+
13
+ Complete rewrite with modern architecture and full documentation (2025).
12
14
 
13
15
  ## Features
14
16
 
@@ -130,14 +132,17 @@ pytest tests/ --cov=detectkit --cov-report=html
130
132
  - ⚠️ Advanced detectors (Prophet, TimesFM) - optional extras
131
133
  - ⚠️ Additional alert channels (Telegram, Email) - optional
132
134
 
133
- See [TODO.md](TODO.md) for detailed development roadmap.
134
-
135
135
  ## Documentation
136
136
 
137
- - [ARCHITECTURE.md](ARCHITECTURE.md) - System architecture and design
138
- - [TECHNICAL_SPEC.md](TECHNICAL_SPEC.md) - Complete technical specification (Russian)
139
- - [TODO.md](TODO.md) - Development roadmap
140
- - [CLAUDE.md](CLAUDE.md) - Development context for AI assistants
137
+ 📚 **Complete documentation available at: https://github.com/alexeiveselov92/detectkit/tree/main/docs**
138
+
139
+ - [Getting Started](https://github.com/alexeiveselov92/detectkit/blob/main/docs/getting-started/quickstart.md) - 5-minute quickstart
140
+ - [Configuration Guide](https://github.com/alexeiveselov92/detectkit/blob/main/docs/guides/configuration.md) - All configuration options
141
+ - [Detectors Guide](https://github.com/alexeiveselov92/detectkit/blob/main/docs/guides/detectors.md) - Choosing the right detector
142
+ - [Alerting Guide](https://github.com/alexeiveselov92/detectkit/blob/main/docs/guides/alerting.md) - Setting up alerts
143
+ - [CLI Reference](https://github.com/alexeiveselov92/detectkit/blob/main/docs/reference/cli.md) - Command-line documentation
144
+ - [Examples](https://github.com/alexeiveselov92/detectkit/tree/main/docs/examples) - Real-world monitoring scenarios
145
+
141
146
 
142
147
  ## Requirements
143
148
 
@@ -12,6 +12,7 @@ import click
12
12
 
13
13
  from detectkit.config.metric_config import MetricConfig
14
14
  from detectkit.config.profile import ProfilesConfig
15
+ from detectkit.config.validator import validate_metric_uniqueness
15
16
  from detectkit.database.internal_tables import InternalTablesManager
16
17
  from detectkit.orchestration.task_manager import PipelineStep, TaskManager
17
18
 
@@ -65,16 +66,37 @@ def run_command(
65
66
  # project_config = load_project_config(project_root)
66
67
 
67
68
  # Select metrics based on selector
68
- metrics = select_metrics(select, project_root)
69
+ # Returns list of (path, config) tuples with uniqueness validation
70
+ try:
71
+ metrics = select_metrics(select, project_root)
72
+ except ValueError as e:
73
+ click.echo(
74
+ click.style(
75
+ f"Error: {e}",
76
+ fg="red",
77
+ bold=True,
78
+ )
79
+ )
80
+ return
69
81
 
70
82
  # Exclude metrics if specified
71
83
  if exclude:
72
- excluded_metrics = select_metrics(exclude, project_root)
73
- excluded_names = {m.name for m in excluded_metrics}
74
- metrics = [m for m in metrics if m.name not in excluded_names]
84
+ try:
85
+ excluded_metrics = select_metrics(exclude, project_root)
86
+ excluded_names = {config.name for _, config in excluded_metrics}
87
+ metrics = [(path, config) for path, config in metrics if config.name not in excluded_names]
75
88
 
76
- if excluded_metrics:
77
- click.echo(f"Excluded {len(excluded_metrics)} metric(s) matching: {exclude}")
89
+ if excluded_metrics:
90
+ click.echo(f"Excluded {len(excluded_metrics)} metric(s) matching: {exclude}")
91
+ except ValueError as e:
92
+ click.echo(
93
+ click.style(
94
+ f"Error in exclusion selector: {e}",
95
+ fg="red",
96
+ bold=True,
97
+ )
98
+ )
99
+ return
78
100
 
79
101
  if not metrics:
80
102
  click.echo(
@@ -150,9 +172,10 @@ def run_command(
150
172
  )
151
173
 
152
174
  # Process each metric
153
- for metric_path in metrics:
175
+ for metric_path, config in metrics:
154
176
  process_metric(
155
177
  metric_path=metric_path,
178
+ config=config,
156
179
  project_root=project_root,
157
180
  task_manager=task_manager,
158
181
  steps=step_list,
@@ -254,9 +277,9 @@ def find_project_root() -> Optional[Path]:
254
277
  return None
255
278
 
256
279
 
257
- def select_metrics(selector: str, project_root: Path) -> List[Path]:
280
+ def select_metrics(selector: str, project_root: Path) -> List[tuple[Path, MetricConfig]]:
258
281
  """
259
- Select metrics based on selector.
282
+ Select metrics based on selector and validate uniqueness.
260
283
 
261
284
  Selector types:
262
285
  - Metric name: "cpu_usage"
@@ -268,34 +291,44 @@ def select_metrics(selector: str, project_root: Path) -> List[Path]:
268
291
  project_root: Project root path
269
292
 
270
293
  Returns:
271
- List of metric file paths
294
+ List of (path, config) tuples for selected metrics
295
+
296
+ Raises:
297
+ ValueError: If duplicate metric names found or configs invalid
272
298
  """
273
299
  metrics_dir = project_root / "metrics"
274
300
 
275
301
  if not metrics_dir.exists():
276
302
  return []
277
303
 
304
+ # Collect metric paths based on selector
305
+ metric_paths: List[Path] = []
306
+
278
307
  # Tag selector
279
308
  if selector.startswith("tag:"):
280
309
  tag = selector[4:]
281
- return find_metrics_by_tag(metrics_dir, tag)
282
-
310
+ metric_paths = find_metrics_by_tag(metrics_dir, tag)
283
311
  # Path pattern selector
284
- if "*" in selector or "/" in selector:
312
+ elif "*" in selector or "/" in selector:
285
313
  pattern = selector if selector.startswith("metrics/") else f"metrics/{selector}"
286
- return list(project_root.glob(pattern))
287
-
288
- # Metric name selector
289
- metric_file = metrics_dir / f"{selector}.yml"
290
- if metric_file.exists():
291
- return [metric_file]
314
+ metric_paths = list(project_root.glob(pattern))
315
+ # Metric name selector (only searches root metrics/ directory)
316
+ else:
317
+ metric_file = metrics_dir / f"{selector}.yml"
318
+ if metric_file.exists():
319
+ metric_paths = [metric_file]
320
+ else:
321
+ # Try with .yaml extension
322
+ metric_file = metrics_dir / f"{selector}.yaml"
323
+ if metric_file.exists():
324
+ metric_paths = [metric_file]
292
325
 
293
- # Try with .yaml extension
294
- metric_file = metrics_dir / f"{selector}.yaml"
295
- if metric_file.exists():
296
- return [metric_file]
326
+ if not metric_paths:
327
+ return []
297
328
 
298
- return []
329
+ # Validate uniqueness and load configs
330
+ # This will raise ValueError if duplicate metric names found
331
+ return validate_metric_uniqueness(metric_paths)
299
332
 
300
333
 
301
334
  def find_metrics_by_tag(metrics_dir: Path, tag: str) -> List[Path]:
@@ -330,6 +363,7 @@ def find_metrics_by_tag(metrics_dir: Path, tag: str) -> List[Path]:
330
363
 
331
364
  def process_metric(
332
365
  metric_path: Path,
366
+ config: MetricConfig,
333
367
  project_root: Path,
334
368
  task_manager: TaskManager,
335
369
  steps: List[PipelineStep],
@@ -343,6 +377,7 @@ def process_metric(
343
377
 
344
378
  Args:
345
379
  metric_path: Path to metric YAML file
380
+ config: Loaded and validated metric configuration
346
381
  project_root: Project root directory
347
382
  task_manager: Task manager instance
348
383
  steps: Pipeline steps to execute
@@ -351,10 +386,11 @@ def process_metric(
351
386
  full_refresh: Full refresh flag
352
387
  force: Force flag
353
388
  """
354
- metric_name = metric_path.stem
389
+ # Use config.name (not metric_path.stem) for consistency
390
+ metric_name = config.name
355
391
 
356
- click.echo(click.style(f"Processing: {metric_name}", fg="cyan", bold=True))
357
- click.echo(f" File: {metric_path}")
392
+ click.echo(click.style(f"Processing metric: {metric_name}", fg="cyan", bold=True))
393
+ click.echo(f" Config file: {metric_path.relative_to(project_root)}")
358
394
  click.echo(f" Steps: {', '.join(s.value for s in steps)}")
359
395
 
360
396
  if from_date:
@@ -368,19 +404,6 @@ def process_metric(
368
404
 
369
405
  click.echo()
370
406
 
371
- # Load metric configuration
372
- try:
373
- config = MetricConfig.from_yaml_file(metric_path)
374
- except Exception as e:
375
- click.echo(
376
- click.style(
377
- f" ✗ Error loading metric config: {e}",
378
- fg="red",
379
- )
380
- )
381
- click.echo()
382
- return
383
-
384
407
  # Run pipeline
385
408
  try:
386
409
  result = task_manager.run_metric(
@@ -231,6 +231,7 @@ class MetricConfig(BaseModel):
231
231
 
232
232
  Attributes:
233
233
  name: Metric name (unique identifier)
234
+ tags: Optional list of tags for metric selection (e.g., ["critical", "api"])
234
235
  profile: Profile name to use (overrides default_profile from project config)
235
236
  query: Inline SQL query (mutually exclusive with query_file)
236
237
  query_file: Path to SQL file (mutually exclusive with query)
@@ -246,6 +247,7 @@ class MetricConfig(BaseModel):
246
247
  Example YAML:
247
248
  ```yaml
248
249
  name: cpu_usage
250
+ tags: ["critical", "infrastructure", "10min"]
249
251
  profile: clickhouse_prod
250
252
  query_file: sql/cpu_usage.sql
251
253
  query_columns:
@@ -275,6 +277,10 @@ class MetricConfig(BaseModel):
275
277
  """
276
278
 
277
279
  name: str = Field(..., description="Metric name")
280
+ tags: Optional[List[str]] = Field(
281
+ default=None,
282
+ description="Optional tags for metric selection (e.g., ['critical', 'api', '10min'])",
283
+ )
278
284
  profile: Optional[str] = Field(
279
285
  default=None, description="Profile name to use (overrides default_profile)"
280
286
  )
@@ -335,6 +341,32 @@ class MetricConfig(BaseModel):
335
341
  )
336
342
  return v
337
343
 
344
+ @field_validator("tags")
345
+ @classmethod
346
+ def validate_tags(cls, v: Optional[List[str]]) -> Optional[List[str]]:
347
+ """Validate tags field."""
348
+ if v is None:
349
+ return v
350
+
351
+ if not v:
352
+ raise ValueError("tags list cannot be empty (use null instead)")
353
+
354
+ # Check for duplicate tags
355
+ if len(v) != len(set(v)):
356
+ raise ValueError("Duplicate tags not allowed")
357
+
358
+ # Validate each tag format (alphanumeric + underscore + dash)
359
+ for tag in v:
360
+ if not tag:
361
+ raise ValueError("Empty tag not allowed")
362
+ if not all(c.isalnum() or c in ("_", "-") for c in tag):
363
+ raise ValueError(
364
+ f"Invalid tag '{tag}': only alphanumeric characters, "
365
+ f"underscores, and dashes allowed"
366
+ )
367
+
368
+ return v
369
+
338
370
  @field_validator("loading_batch_size")
339
371
  @classmethod
340
372
  def validate_batch_size(cls, v: int) -> int:
@@ -0,0 +1,124 @@
1
+ """
2
+ Metric configuration validation.
3
+
4
+ This module provides validation functions for metric configurations,
5
+ ensuring data integrity and preventing configuration errors.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from typing import List, Tuple
10
+
11
+ from detectkit.config.metric_config import MetricConfig
12
+
13
+
14
+ def validate_metric_uniqueness(metric_paths: List[Path]) -> List[Tuple[Path, MetricConfig]]:
15
+ """
16
+ Load all metrics and validate that metric names are unique.
17
+
18
+ This validation is CRITICAL for data integrity because duplicate metric names
19
+ would cause:
20
+ - Data corruption (mixed data in _dtk_datapoints table)
21
+ - Task blocking (lock conflicts in _dtk_tasks table)
22
+ - Wrong anomaly detection (detectors receive mixed data from different sources)
23
+ - Data loss (ReplacingMergeTree ignores duplicate inserts)
24
+
25
+ Args:
26
+ metric_paths: List of paths to metric YAML files
27
+
28
+ Returns:
29
+ List of (path, config) tuples for all valid metrics
30
+
31
+ Raises:
32
+ ValueError: If duplicate metric names are found, with clear error message
33
+ showing which files have conflicting names
34
+ ValidationError: If any metric config fails to parse
35
+
36
+ Example:
37
+ >>> paths = [Path("metrics/api/cpu.yml"), Path("metrics/system/cpu.yml")]
38
+ >>> validate_metric_uniqueness(paths)
39
+ ValueError: Duplicate metric name 'cpu_usage' found:
40
+ - metrics/api/cpu.yml
41
+ - metrics/system/cpu.yml
42
+
43
+ Metric names must be unique across the project.
44
+ Please rename one of the metrics.
45
+ """
46
+ configs: List[Tuple[Path, MetricConfig]] = []
47
+ seen_names: dict[str, Path] = {}
48
+
49
+ for metric_path in metric_paths:
50
+ # Load and parse config
51
+ try:
52
+ config = MetricConfig.from_yaml_file(metric_path)
53
+ except Exception as e:
54
+ raise ValueError(
55
+ f"Failed to parse metric config at {metric_path}:\n{e}"
56
+ ) from e
57
+
58
+ # Check for duplicate metric names
59
+ if config.name in seen_names:
60
+ conflicting_path = seen_names[config.name]
61
+ raise ValueError(
62
+ f"Duplicate metric name '{config.name}' found:\n"
63
+ f" - {conflicting_path}\n"
64
+ f" - {metric_path}\n\n"
65
+ f"Metric names must be unique across the project.\n"
66
+ f"Please rename one of the metrics to avoid data corruption."
67
+ )
68
+
69
+ seen_names[config.name] = metric_path
70
+ configs.append((metric_path, config))
71
+
72
+ return configs
73
+
74
+
75
+ def validate_project_metrics(project_root: Path) -> List[Tuple[Path, MetricConfig]]:
76
+ """
77
+ Load and validate all metrics in the project.
78
+
79
+ This is a convenience function that:
80
+ 1. Finds all *.yml and *.yaml files in the metrics/ directory (recursively)
81
+ 2. Validates uniqueness of metric names
82
+ 3. Returns validated list of (path, config) tuples
83
+
84
+ Args:
85
+ project_root: Path to project root directory (contains metrics/ folder)
86
+
87
+ Returns:
88
+ List of (path, config) tuples for all valid metrics
89
+
90
+ Raises:
91
+ ValueError: If duplicate metric names found or configs fail validation
92
+ FileNotFoundError: If metrics/ directory doesn't exist
93
+
94
+ Example:
95
+ >>> from pathlib import Path
96
+ >>> project_root = Path("/path/to/project")
97
+ >>> metrics = validate_project_metrics(project_root)
98
+ >>> for path, config in metrics:
99
+ ... print(f"{config.name}: {path}")
100
+ """
101
+ metrics_dir = project_root / "metrics"
102
+
103
+ if not metrics_dir.exists():
104
+ raise FileNotFoundError(
105
+ f"Metrics directory not found: {metrics_dir}\n"
106
+ f"Expected structure:\n"
107
+ f" {project_root}/\n"
108
+ f" metrics/\n"
109
+ f" your_metric.yml\n"
110
+ )
111
+
112
+ # Find all metric files recursively
113
+ metric_paths = []
114
+ for pattern in ["**/*.yml", "**/*.yaml"]:
115
+ metric_paths.extend(metrics_dir.glob(pattern))
116
+
117
+ if not metric_paths:
118
+ raise ValueError(
119
+ f"No metric files found in {metrics_dir}\n"
120
+ f"Expected at least one *.yml or *.yaml file."
121
+ )
122
+
123
+ # Validate uniqueness
124
+ return validate_metric_uniqueness(metric_paths)