Coverage for functions \ flipdare \ analysis \ data \ nested \ _time_series_nested_data.py: 92%
48 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
1#!/usr/bin/env python
2# Copyright (c) 2026 Flipdare Pty Ltd. All rights reserved.
3#
4# This file is part of Flipdare's proprietary software and contains
5# confidential and copyrighted material. Unauthorised copying,
6# modification, distribution, or use of this file is strictly
7# prohibited without prior written permission from Flipdare Pty Ltd.
8#
9# This software includes third-party components licensed under MIT,
10# BSD, and Apache 2.0 licences. See THIRD_PARTY_NOTICES for details.
11#
13from abc import ABC, abstractmethod
14from typing import override
15from datetime import datetime, timedelta
16from collections import defaultdict
17from dataclasses import dataclass, field
18from flipdare.analysis.data._time_series_protocol import TimeSeriesProtocol, TimeSeriesPlotInfo
19from flipdare.app_types import ReportListType
21__all__ = ["TimeSeriesNestedData"]
23_DAY = timedelta(days=1)
26@dataclass
27class TimeSeriesNestedData[K, V](ABC, TimeSeriesProtocol):
28 """
29 Base for time series with (datetime, K) -> V storage.
31 All datetime keys are normalised to midnight UTC via `_to_day`.
32 `dates` fills every 1-day interval between the earliest and latest key.
33 Subclasses must implement `headers`, `table_data`, `plot_info`, and `add`.
34 """
36 _rows: defaultdict[datetime, dict[K, V]] = field(
37 default_factory=lambda: defaultdict(dict),
38 init=False,
39 repr=False,
40 )
42 _error_ct: int = field(default=0, init=False, repr=False)
44 # NOTE: these must be overridden
45 @property
46 @abstractmethod
47 @override
48 def headers(self) -> list[str]: ...
50 @abstractmethod
51 @override
52 def table_data(self) -> ReportListType: ...
54 @abstractmethod
55 @override
56 def plot_info(self) -> list[TimeSeriesPlotInfo]: ...
58 # helpers
59 @property
60 def error_count(self) -> int:
61 return self._error_ct
63 def increment_error(self) -> None:
64 self._error_ct += 1
66 @staticmethod
67 def _to_day(dt: datetime) -> datetime:
68 """Normalise a datetime to midnight UTC for use as a row key."""
69 return dt.replace(hour=0, minute=0, second=0, microsecond=0)
71 @property
72 @override
73 def has_data(self) -> bool:
74 return any(bool(row) for row in self._rows.values())
76 @property
77 @override
78 def count(self) -> int:
79 return sum(len(row) for row in self._rows.values())
81 @property
82 @override
83 def dates(self) -> list[datetime]:
84 """All daily dates from earliest to latest, padding days with no data."""
85 keys = sorted(self._rows.keys())
86 if not keys:
87 return []
88 result: list[datetime] = []
89 current = keys[0]
90 end = keys[-1]
91 while current <= end:
92 result.append(current)
93 current += _DAY
94 return result
96 @property
97 @override
98 def debug_str(self) -> str:
99 lines: list[str] = [", ".join(self.headers)]
100 lines.extend(", ".join(str(v) for v in row) for row in self.table_data())
101 return "\n".join(lines)