global: reorg fixes + clients improved

This commit is contained in:
nym21
2026-01-28 23:35:51 +01:00
parent fecaf0f400
commit 6709ded66c
55 changed files with 4312 additions and 83 deletions

View File

@@ -131,16 +131,114 @@ def _p(prefix: str, acc: str) -> str:
pub fn generate_endpoint_class(output: &mut String) {
writeln!(
output,
r#"@dataclass
r#"# Date conversion constants
_GENESIS = date(2009, 1, 3) # dateindex 0, weekindex 0
_DAY_ONE = date(2009, 1, 9) # dateindex 1 (6 day gap after genesis)
_DATE_INDEXES = frozenset(['dateindex', 'weekindex', 'monthindex', 'yearindex', 'quarterindex', 'semesterindex', 'decadeindex'])
def is_date_index(index: str) -> bool:
"""Check if an index type is date-based."""
return index in _DATE_INDEXES
def index_to_date(index: str, i: int) -> date:
"""Convert an index value to a date for date-based indexes."""
if index == 'dateindex':
return _GENESIS if i == 0 else _DAY_ONE + timedelta(days=i - 1)
elif index == 'weekindex':
return _GENESIS + timedelta(weeks=i)
elif index == 'monthindex':
return date(2009 + i // 12, i % 12 + 1, 1)
elif index == 'yearindex':
return date(2009 + i, 1, 1)
elif index == 'quarterindex':
m = i * 3
return date(2009 + m // 12, m % 12 + 1, 1)
elif index == 'semesterindex':
m = i * 6
return date(2009 + m // 12, m % 12 + 1, 1)
elif index == 'decadeindex':
return date(2009 + i * 10, 1, 1)
else:
raise ValueError(f"{{index}} is not a date-based index")
@dataclass
class MetricData(Generic[T]):
"""Metric data with range information."""
version: int
index: Index
total: int
start: int
end: int
stamp: str
data: List[T]
def dates(self) -> List[date]:
"""Convert index range to dates. Only works for date-based indexes."""
return [index_to_date(self.index, i) for i in range(self.start, self.end)]
def indexes(self) -> List[int]:
"""Get index range as list."""
return list(range(self.start, self.end))
def to_date_dict(self) -> dict[date, T]:
"""Return data as {{date: value}} dict. Only works for date-based indexes."""
return dict(zip(self.dates(), self.data))
def to_index_dict(self) -> dict[int, T]:
"""Return data as {{index: value}} dict."""
return dict(zip(range(self.start, self.end), self.data))
def date_items(self) -> List[Tuple[date, T]]:
"""Return data as [(date, value), ...] pairs. Only works for date-based indexes."""
return list(zip(self.dates(), self.data))
def index_items(self) -> List[Tuple[int, T]]:
"""Return data as [(index, value), ...] pairs."""
return list(zip(range(self.start, self.end), self.data))
def iter(self) -> Iterator[Tuple[int, T]]:
"""Iterate over (index, value) pairs."""
return iter(zip(range(self.start, self.end), self.data))
def iter_dates(self) -> Iterator[Tuple[date, T]]:
"""Iterate over (date, value) pairs. Date-based indexes only."""
return iter(zip(self.dates(), self.data))
def __iter__(self) -> Iterator[Tuple[int, T]]:
"""Default iteration over (index, value) pairs."""
return self.iter()
def to_polars(self, with_dates: bool = True) -> pl.DataFrame:
"""Convert to Polars DataFrame. Requires polars to be installed.
Returns a DataFrame with columns:
- 'date' (date) and 'value' (T) if with_dates=True and index is date-based
- 'index' (int) and 'value' (T) otherwise
"""
try:
import polars as pl # type: ignore[import-not-found]
except ImportError:
raise ImportError("polars is required: pip install polars")
if with_dates and self.index in _DATE_INDEXES:
return pl.DataFrame({{"date": self.dates(), "value": self.data}})
return pl.DataFrame({{"index": list(range(self.start, self.end)), "value": self.data}})
def to_pandas(self, with_dates: bool = True) -> pd.DataFrame:
"""Convert to Pandas DataFrame. Requires pandas to be installed.
Returns a DataFrame with columns:
- 'date' (date) and 'value' (T) if with_dates=True and index is date-based
- 'index' (int) and 'value' (T) otherwise
"""
try:
import pandas as pd # type: ignore[import-not-found]
except ImportError:
raise ImportError("pandas is required: pip install pandas")
if with_dates and self.index in _DATE_INDEXES:
return pd.DataFrame({{"date": self.dates(), "value": self.data}})
return pd.DataFrame({{"index": list(range(self.start, self.end)), "value": self.data}})
# Type alias for non-generic usage
AnyMetricData = MetricData[Any]
@@ -177,9 +275,8 @@ class _EndpointConfig:
p = self.path()
return f"{{p}}?{{query}}" if query else p
def get_json(self) -> MetricData:
data = self.client.get_json(self._build_path())
return MetricData(**data)
def get_metric(self) -> MetricData:
return MetricData(**self.client.get_json(self._build_path()))
def get_csv(self) -> str:
return self.client.get_text(self._build_path(format='csv'))
@@ -193,7 +290,7 @@ class RangeBuilder(Generic[T]):
def fetch(self) -> MetricData[T]:
"""Fetch the range as parsed JSON."""
return self._config.get_json()
return self._config.get_metric()
def fetch_csv(self) -> str:
"""Fetch the range as CSV string."""
@@ -208,7 +305,7 @@ class SingleItemBuilder(Generic[T]):
def fetch(self) -> MetricData[T]:
"""Fetch the single item."""
return self._config.get_json()
return self._config.get_metric()
def fetch_csv(self) -> str:
"""Fetch as CSV."""
@@ -231,7 +328,7 @@ class SkippedBuilder(Generic[T]):
def fetch(self) -> MetricData[T]:
"""Fetch from skipped position to end."""
return self._config.get_json()
return self._config.get_metric()
def fetch_csv(self) -> str:
"""Fetch as CSV."""
@@ -315,7 +412,7 @@ class MetricEndpointBuilder(Generic[T]):
def fetch(self) -> MetricData[T]:
"""Fetch all data as parsed JSON."""
return self._config.get_json()
return self._config.get_metric()
def fetch_csv(self) -> str:
"""Fetch all data as CSV string."""