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
@@ -48,15 +48,120 @@ class BrkError extends Error {{
}}
}}
// Date conversion constants and helpers
const _GENESIS = new Date(2009, 0, 3); // dateindex 0, weekindex 0
const _DAY_ONE = new Date(2009, 0, 9); // dateindex 1 (6 day gap after genesis)
const _MS_PER_DAY = 24 * 60 * 60 * 1000;
const _MS_PER_WEEK = 7 * _MS_PER_DAY;
const _DATE_INDEXES = new Set(['dateindex', 'weekindex', 'monthindex', 'yearindex', 'quarterindex', 'semesterindex', 'decadeindex']);
/** @param {{number}} months @returns {{globalThis.Date}} */
const _addMonths = (months) => new Date(2009, months, 1);
/**
* Convert an index value to a Date for date-based indexes.
* @param {{Index}} index - The index type
* @param {{number}} i - The index value
* @returns {{globalThis.Date}}
*/
function indexToDate(index, i) {{
switch (index) {{
case 'dateindex': return i === 0 ? _GENESIS : new Date(_DAY_ONE.getTime() + (i - 1) * _MS_PER_DAY);
case 'weekindex': return new Date(_GENESIS.getTime() + i * _MS_PER_WEEK);
case 'monthindex': return _addMonths(i);
case 'yearindex': return new Date(2009 + i, 0, 1);
case 'quarterindex': return _addMonths(i * 3);
case 'semesterindex': return _addMonths(i * 6);
case 'decadeindex': return new Date(2009 + i * 10, 0, 1);
default: throw new Error(`${{index}} is not a date-based index`);
}}
}}
/**
* Check if an index type is date-based.
* @param {{Index}} index
* @returns {{boolean}}
*/
function isDateIndex(index) {{
return _DATE_INDEXES.has(index);
}}
/**
* Wrap raw metric data with helper methods.
* @template T
* @param {{MetricData<T>}} raw - Raw JSON response
* @returns {{MetricData<T>}}
*/
function _wrapMetricData(raw) {{
const {{ index, start, end, data }} = raw;
return /** @type {{MetricData<T>}} */ ({{
...raw,
dates() {{
/** @type {{globalThis.Date[]}} */
const result = [];
for (let i = start; i < end; i++) result.push(indexToDate(index, i));
return result;
}},
indexes() {{
/** @type {{number[]}} */
const result = [];
for (let i = start; i < end; i++) result.push(i);
return result;
}},
toDateMap() {{
/** @type {{Map<globalThis.Date, T>}} */
const map = new Map();
for (let i = 0; i < data.length; i++) map.set(indexToDate(index, start + i), data[i]);
return map;
}},
toIndexMap() {{
/** @type {{Map<number, T>}} */
const map = new Map();
for (let i = 0; i < data.length; i++) map.set(start + i, data[i]);
return map;
}},
dateEntries() {{
/** @type {{Array<[globalThis.Date, T]>}} */
const result = [];
for (let i = 0; i < data.length; i++) result.push([indexToDate(index, start + i), data[i]]);
return result;
}},
indexEntries() {{
/** @type {{Array<[number, T]>}} */
const result = [];
for (let i = 0; i < data.length; i++) result.push([start + i, data[i]]);
return result;
}},
*iter() {{
for (let i = 0; i < data.length; i++) yield [start + i, data[i]];
}},
*iterDates() {{
for (let i = 0; i < data.length; i++) yield [indexToDate(index, start + i), data[i]];
}},
[Symbol.iterator]() {{
return this.iter();
}},
}});
}}
/**
* @template T
* @typedef {{Object}} MetricData
* @property {{number}} version - Version of the metric data
* @property {{Index}} index - The index type used for this query
* @property {{number}} total - Total number of data points
* @property {{number}} start - Start index (inclusive)
* @property {{number}} end - End index (exclusive)
* @property {{string}} stamp - ISO 8601 timestamp of when the response was generated
* @property {{T[]}} data - The metric data
* @property {{() => globalThis.Date[]}} dates - Convert index range to dates (date-based indexes only)
* @property {{() => number[]}} indexes - Get index range as array
* @property {{() => Map<globalThis.Date, T>}} toDateMap - Return data as Map keyed by date (date-based only)
* @property {{() => Map<number, T>}} toIndexMap - Return data as Map keyed by index
* @property {{() => Array<[globalThis.Date, T]>}} dateEntries - Return data as [date, value] pairs (date-based only)
* @property {{() => Array<[number, T]>}} indexEntries - Return data as [index, value] pairs
* @property {{() => IterableIterator<[number, T]>}} iter - Iterate over [index, value] pairs
* @property {{() => IterableIterator<[globalThis.Date, T]>}} iterDates - Iterate over [date, value] pairs (date-based only)
*/
/** @typedef {{MetricData<any>}} AnyMetricData */
@@ -150,18 +255,18 @@ function _endpoint(client, name, index) {{
* @returns {{RangeBuilder<T>}}
*/
const rangeBuilder = (start, end) => ({{
fetch(onUpdate) {{ return client.getJson(buildPath(start, end), onUpdate); }},
fetch(onUpdate) {{ return client._fetchMetricData(buildPath(start, end), onUpdate); }},
fetchCsv() {{ return client.getText(buildPath(start, end, 'csv')); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
}});
/**
* @param {{number}} index
* @param {{number}} idx
* @returns {{SingleItemBuilder<T>}}
*/
const singleItemBuilder = (index) => ({{
fetch(onUpdate) {{ return client.getJson(buildPath(index, index + 1), onUpdate); }},
fetchCsv() {{ return client.getText(buildPath(index, index + 1, 'csv')); }},
const singleItemBuilder = (idx) => ({{
fetch(onUpdate) {{ return client._fetchMetricData(buildPath(idx, idx + 1), onUpdate); }},
fetchCsv() {{ return client.getText(buildPath(idx, idx + 1, 'csv')); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
}});
@@ -171,19 +276,19 @@ function _endpoint(client, name, index) {{
*/
const skippedBuilder = (start) => ({{
take(n) {{ return rangeBuilder(start, start + n); }},
fetch(onUpdate) {{ return client.getJson(buildPath(start, undefined), onUpdate); }},
fetch(onUpdate) {{ return client._fetchMetricData(buildPath(start, undefined), onUpdate); }},
fetchCsv() {{ return client.getText(buildPath(start, undefined, 'csv')); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
}});
/** @type {{MetricEndpointBuilder<T>}} */
const endpoint = {{
get(index) {{ return singleItemBuilder(index); }},
get(idx) {{ return singleItemBuilder(idx); }},
slice(start, end) {{ return rangeBuilder(start, end); }},
first(n) {{ return rangeBuilder(undefined, n); }},
last(n) {{ return n === 0 ? rangeBuilder(undefined, 0) : rangeBuilder(-n, undefined); }},
skip(n) {{ return skippedBuilder(n); }},
fetch(onUpdate) {{ return client.getJson(buildPath(), onUpdate); }},
fetch(onUpdate) {{ return client._fetchMetricData(buildPath(), onUpdate); }},
fetchCsv() {{ return client.getText(buildPath(undefined, undefined, 'csv')); }},
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
get path() {{ return p; }},
@@ -263,6 +368,19 @@ class BrkClientBase {{
const res = await this.get(path);
return res.text();
}}
/**
* Fetch metric data and wrap with helper methods (internal)
* @template T
* @param {{string}} path
* @param {{(value: MetricData<T>) => void}} [onUpdate]
* @returns {{Promise<MetricData<T>>}}
*/
async _fetchMetricData(path, onUpdate) {{
const wrappedOnUpdate = onUpdate ? (/** @type {{MetricData<T>}} */ raw) => onUpdate(_wrapMetricData(raw)) : undefined;
const raw = await this.getJson(path, wrappedOnUpdate);
return _wrapMetricData(raw);
}}
}}
/**
@@ -299,6 +417,31 @@ pub fn generate_static_constants(output: &mut String) {
for (name, value) in CohortConstants::all() {
write_static_const(output, name, &format_json(&camel_case_keys(value)));
}
// Helper methods
writeln!(
output,
r#" /**
* Convert an index value to a Date for date-based indexes.
* @param {{Index}} index - The index type
* @param {{number}} i - The index value
* @returns {{globalThis.Date}}
*/
indexToDate(index, i) {{
return indexToDate(index, i);
}}
/**
* Check if an index type is date-based.
* @param {{Index}} index
* @returns {{boolean}}
*/
isDateIndex(index) {{
return isDateIndex(index);
}}
"#
)
.unwrap();
}
fn indent_json_const(json: &str) -> String {
@@ -39,6 +39,16 @@ pub fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) {
writeln!(output, " return MetricEndpointBuilder(self, metric, index)").unwrap();
writeln!(output).unwrap();
// Generate helper methods
writeln!(output, " def index_to_date(self, index: Index, i: int) -> date:").unwrap();
writeln!(output, " \"\"\"Convert an index value to a date for date-based indexes.\"\"\"").unwrap();
writeln!(output, " return index_to_date(index, i)").unwrap();
writeln!(output).unwrap();
writeln!(output, " def is_date_index(self, index: Index) -> bool:").unwrap();
writeln!(output, " \"\"\"Check if an index type is date-based.\"\"\"").unwrap();
writeln!(output, " return is_date_index(index)").unwrap();
writeln!(output).unwrap();
// Generate API methods
generate_api_methods(output, endpoints);
}
@@ -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."""
@@ -29,7 +29,7 @@ pub fn generate_python_client(
writeln!(output, "from dataclasses import dataclass").unwrap();
writeln!(
output,
"from typing import TypeVar, Generic, Any, Optional, List, Literal, TypedDict, Union, Protocol, overload"
"from typing import TypeVar, Generic, Any, Optional, List, Literal, TypedDict, Union, Protocol, overload, Iterator, Tuple, TYPE_CHECKING"
)
.unwrap();
writeln!(
@@ -38,7 +38,11 @@ pub fn generate_python_client(
)
.unwrap();
writeln!(output, "from urllib.parse import urlparse").unwrap();
writeln!(output, "from datetime import date, timedelta").unwrap();
writeln!(output, "import json\n").unwrap();
writeln!(output, "if TYPE_CHECKING:").unwrap();
writeln!(output, " import pandas as pd # type: ignore[import-not-found]").unwrap();
writeln!(output, " import polars as pl # type: ignore[import-not-found]\n").unwrap();
writeln!(output, "T = TypeVar('T')\n").unwrap();
types::generate_type_definitions(&mut output, schemas);