70 lines
1.8 KiB
Python
70 lines
1.8 KiB
Python
import dataclasses
|
|
import datetime
|
|
from collections.abc import Iterator, Sequence
|
|
|
|
HIDDEN_LABEL_CATEGORY = '__'
|
|
DEFAULT_ESTIMATED_DURATION = datetime.timedelta(hours=1)
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True, order=True)
|
|
class Label:
|
|
category: str
|
|
label: str
|
|
|
|
def __post_init__(self):
|
|
assert self.category is not None
|
|
assert ':' not in self.category
|
|
assert self.label is not None
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True, order=True)
|
|
class ActivitySample:
|
|
labels: Sequence[Label]
|
|
start_at: datetime.datetime | None
|
|
end_at: datetime.datetime | None
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True, order=True)
|
|
class RealizedActivitySample(ActivitySample):
|
|
start_at: datetime.datetime
|
|
end_at: datetime.datetime
|
|
|
|
|
|
def heuristically_realize_samples(
|
|
samples: list[ActivitySample],
|
|
) -> Iterator[RealizedActivitySample]:
|
|
"""Secret sauce.
|
|
|
|
Guarentees that:
|
|
* No samples overlap.
|
|
"""
|
|
|
|
previous_sample_end = None
|
|
for sample in samples:
|
|
end_at = sample.end_at
|
|
|
|
if previous_sample_end is None:
|
|
if end_at.tzinfo:
|
|
previous_sample_end = datetime.datetime.fromtimestamp(0, datetime.UTC)
|
|
else:
|
|
previous_sample_end = datetime.datetime.fromtimestamp(0)
|
|
|
|
assert previous_sample_end <= end_at, 'Iterating in incorrect order'
|
|
|
|
# TODO: Allow end_at is None
|
|
|
|
start_at = sample.start_at
|
|
if start_at is None:
|
|
estimated_duration: datetime.timedelta = DEFAULT_ESTIMATED_DURATION
|
|
start_at = max(previous_sample_end, end_at - estimated_duration)
|
|
del estimated_duration
|
|
|
|
yield RealizedActivitySample(
|
|
labels=sample.labels,
|
|
end_at=end_at,
|
|
start_at=start_at,
|
|
)
|
|
|
|
previous_sample_end = sample.end_at
|
|
del sample
|