lib/metric-config-parser/metric_config_parser/project.py (108 lines of code) (raw):
import enum
from datetime import datetime
from typing import TYPE_CHECKING, Any, List, Optional
import attr
if TYPE_CHECKING:
from metric_config_parser.config import ConfigCollection
from metric_config_parser.monitoring import MonitoringSpec
from metric_config_parser.alert import AlertReference
from metric_config_parser.experiment import Experiment
from metric_config_parser.metric import MetricReference
from metric_config_parser.metric_group import MetricGroup, MetricGroupsSpec
from metric_config_parser.population import PopulationConfiguration, PopulationSpec
from metric_config_parser.util import converter, parse_date
class MonitoringPeriod(enum.Enum):
"""
Monitoring period.
Used as x-axis.
"""
BUILD_ID = "build_id"
DAY = "submission_date"
@attr.s(auto_attribs=True, kw_only=True)
class ProjectConfiguration:
"""Describes the interface for defining the project in configuration."""
reference_branch: str = "control"
app_name: str = "firefox_desktop"
name: Optional[str] = None
xaxis: MonitoringPeriod = attr.ib(default=MonitoringPeriod.DAY)
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
population: PopulationConfiguration = attr.Factory(PopulationConfiguration)
compact_visualization: bool = False
skip_default_metrics: bool = False
skip: bool = False
is_rollout: bool = False
metric_groups: List[MetricGroup] = attr.Factory(list)
def _validate_yyyy_mm_dd(instance: Any, attribute: Any, value: Any) -> None:
parse_date(value)
@attr.s(auto_attribs=True, kw_only=True)
class ProjectSpec:
"""Describes the interface for defining the project."""
name: Optional[str] = None
platform: Optional[str] = None
xaxis: Optional[MonitoringPeriod] = None
start_date: Optional[str] = attr.ib(default=None, validator=_validate_yyyy_mm_dd)
end_date: Optional[str] = attr.ib(default=None, validator=_validate_yyyy_mm_dd)
metrics: List[MetricReference] = attr.Factory(list)
alerts: List[AlertReference] = attr.Factory(list)
reference_branch: Optional[str] = None
population: PopulationSpec = attr.Factory(PopulationSpec)
compact_visualization: bool = False
skip_default_metrics: bool = False
skip: bool = False
is_rollout: bool = False
metric_groups: MetricGroupsSpec = attr.Factory(MetricGroupsSpec)
@classmethod
def from_dict(cls, d: dict) -> "ProjectSpec":
"""Create a new `ProjectSpec` from a dictionary."""
d = dict((k.lower(), v) for k, v in d.items())
return converter.structure(d, cls)
def resolve(
self,
spec: "MonitoringSpec",
experiment: Optional["Experiment"],
configs: "ConfigCollection",
) -> ProjectConfiguration:
"""Create a `ProjectConfiguration` from the spec."""
project_config = ProjectConfiguration(
name=self.name or (experiment.normandy_slug if experiment else None),
xaxis=self.xaxis or MonitoringPeriod.DAY,
start_date=parse_date(
self.start_date
or (
experiment.start_date.strftime("%Y-%m-%d")
if experiment and experiment.start_date
else None
)
),
end_date=parse_date(
self.end_date
or (
experiment.end_date.strftime("%Y-%m-%d")
if experiment and experiment.end_date
else None
)
),
reference_branch=self.reference_branch
or (
experiment.reference_branch
if experiment and experiment.reference_branch
else "control"
),
compact_visualization=self.compact_visualization,
skip_default_metrics=self.skip_default_metrics,
skip=self.skip,
app_name=self.platform or "firefox_desktop",
is_rollout=self.is_rollout,
metric_groups=[],
)
project_config.population = self.population.resolve(spec, project_config, configs)
metric_groups = []
for group_ref in [d for _, d in self.metric_groups.definitions.items()]:
metric_groups.append(group_ref.resolve(spec, project_config, configs))
project_config.metric_groups = metric_groups
return project_config
def merge(self, other: "ProjectSpec") -> None:
"""
Merge another project spec into the current one.
The `other` ProjectSpec overwrites existing keys.
"""
for key in attr.fields_dict(type(self)):
if key == "population":
self.population.merge(other.population)
elif key == "metrics":
self.metrics += other.metrics
elif key == "alerts":
self.alerts += other.alerts
elif key == "metric_groups":
self.metric_groups.merge(other.metric_groups)
else:
setattr(self, key, getattr(other, key) or getattr(self, key))