global: snapshot

This commit is contained in:
nym21
2026-02-24 12:21:20 +01:00
parent 3b7aa8242a
commit cefc8cfd42
24 changed files with 3561 additions and 1073 deletions
@@ -128,20 +128,14 @@ function dateToIndex(index, d) {{
* Wrap raw metric data with helper methods.
* @template T
* @param {{MetricData<T>}} raw - Raw JSON response
* @returns {{MetricData<T>}}
* @returns {{DateMetricData<T>}}
*/
function _wrapMetricData(raw) {{
const {{ index, start, end, data }} = raw;
const _dateBased = _DATE_INDEXES.has(index);
return /** @type {{MetricData<T>}} */ ({{
return /** @type {{DateMetricData<T>}} */ ({{
...raw,
isDateBased: _dateBased,
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 = [];
@@ -149,41 +143,48 @@ function _wrapMetricData(raw) {{
return result;
}},
keys() {{
return _dateBased ? this.dates() : this.indexes();
return this.indexes();
}},
entries() {{
/** @type {{Array<[globalThis.Date | number, T]>}} */
/** @type {{Array<[number, T]>}} */
const result = [];
if (_dateBased) {{
for (let i = 0; i < data.length; i++) result.push([indexToDate(index, start + i), data[i]]);
}} else {{
for (let i = 0; i < data.length; i++) result.push([start + i, data[i]]);
}}
for (let i = 0; i < data.length; i++) result.push([start + i, data[i]]);
return result;
}},
toMap() {{
/** @type {{Map<globalThis.Date | number, T>}} */
/** @type {{Map<number, T>}} */
const map = new Map();
if (_dateBased) {{
for (let i = 0; i < data.length; i++) map.set(indexToDate(index, start + i), data[i]);
}} else {{
for (let i = 0; i < data.length; i++) map.set(start + i, data[i]);
}}
for (let i = 0; i < data.length; i++) map.set(start + i, data[i]);
return map;
}},
*[Symbol.iterator]() {{
if (_dateBased) {{
for (let i = 0; i < data.length; i++) yield [indexToDate(index, start + i), data[i]];
}} else {{
for (let i = 0; i < data.length; i++) yield [start + i, data[i]];
}}
for (let i = 0; i < data.length; i++) yield /** @type {{[number, T]}} */ ([start + i, data[i]]);
}},
// DateMetricData methods (only meaningful for date-based indexes)
dates() {{
/** @type {{globalThis.Date[]}} */
const result = [];
for (let i = start; i < end; i++) result.push(indexToDate(index, i));
return result;
}},
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;
}},
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;
}},
}});
}}
/**
* @template T
* @typedef {{Object}} MetricData
* @typedef {{Object}} MetricDataBase
* @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
@@ -192,26 +193,33 @@ function _wrapMetricData(raw) {{
* @property {{string}} stamp - ISO 8601 timestamp of when the response was generated
* @property {{T[]}} data - The metric data
* @property {{boolean}} isDateBased - Whether this metric uses a date-based index
* @property {{() => (globalThis.Date[] | number[])}} keys - Get keys (dates for date-based, index numbers otherwise)
* @property {{() => Array<[globalThis.Date | number, T]>}} entries - Get [key, value] pairs (dates for date-based, index numbers otherwise)
* @property {{() => Map<globalThis.Date | number, T>}} toMap - Return data as Map (dates for date-based, index numbers otherwise)
* @property {{() => globalThis.Date[]}} dates - Get dates (date-based indexes only, throws otherwise)
* @property {{() => number[]}} indexes - Get index numbers
* @property {{() => number[]}} keys - Get keys as index numbers (alias for indexes)
* @property {{() => Array<[number, T]>}} entries - Get [index, value] pairs
* @property {{() => Map<number, T>}} toMap - Convert to Map<index, value>
*/
/** @template T @typedef {{MetricDataBase<T> & Iterable<[number, T]>}} MetricData */
/**
* @template T
* @typedef {{Object}} DateMetricDataExtras
* @property {{() => globalThis.Date[]}} dates - Get dates for each data point
* @property {{() => Array<[globalThis.Date, T]>}} dateEntries - Get [date, value] pairs
* @property {{() => Map<globalThis.Date, T>}} toDateMap - Convert to Map<date, value>
*/
/** @template T @typedef {{MetricData<T> & DateMetricDataExtras<T>}} DateMetricData */
/** @typedef {{MetricData<any>}} AnyMetricData */
/**
* Thenable interface for await support.
* @template T
* @typedef {{(onfulfilled?: (value: MetricData<T>) => MetricData<T>, onrejected?: (reason: Error) => never) => Promise<MetricData<T>>}} Thenable
*/
/** @template T @typedef {{(onfulfilled?: (value: MetricData<T>) => any, onrejected?: (reason: Error) => never) => Promise<MetricData<T>>}} Thenable */
/** @template T @typedef {{(onfulfilled?: (value: DateMetricData<T>) => any, onrejected?: (reason: Error) => never) => Promise<DateMetricData<T>>}} DateThenable */
/**
* Metric endpoint builder. Callable (returns itself) so both .by.day1 and .by.day1() work.
* @template T
* @typedef {{Object}} MetricEndpointBuilder
* @property {{(index: number) => SingleItemBuilder<T>}} get - Get single item at index
* @property {{(start?: number | globalThis.Date, end?: number | globalThis.Date) => RangeBuilder<T>}} slice - Slice by index or Date
* @property {{(start?: number, end?: number) => RangeBuilder<T>}} slice - Slice by index
* @property {{(n: number) => RangeBuilder<T>}} first - Get first n items
* @property {{(n: number) => RangeBuilder<T>}} last - Get last n items
* @property {{(n: number) => SkippedBuilder<T>}} skip - Skip first n items, chain with take()
@@ -220,38 +228,66 @@ function _wrapMetricData(raw) {{
* @property {{Thenable<T>}} then - Thenable (await endpoint)
* @property {{string}} path - The endpoint path
*/
/** @typedef {{MetricEndpointBuilder<any>}} AnyMetricEndpointBuilder */
/**
* @template T
* @typedef {{Object}} SingleItemBuilder
* @typedef {{Object}} DateMetricEndpointBuilder
* @property {{(index: number | globalThis.Date) => DateSingleItemBuilder<T>}} get - Get single item at index or Date
* @property {{(start?: number | globalThis.Date, end?: number | globalThis.Date) => DateRangeBuilder<T>}} slice - Slice by index or Date
* @property {{(n: number) => DateRangeBuilder<T>}} first - Get first n items
* @property {{(n: number) => DateRangeBuilder<T>}} last - Get last n items
* @property {{(n: number) => DateSkippedBuilder<T>}} skip - Skip first n items, chain with take()
* @property {{(onUpdate?: (value: DateMetricData<T>) => void) => Promise<DateMetricData<T>>}} fetch - Fetch all data
* @property {{() => Promise<string>}} fetchCsv - Fetch all data as CSV
* @property {{DateThenable<T>}} then - Thenable (await endpoint)
* @property {{string}} path - The endpoint path
*/
/** @typedef {{MetricEndpointBuilder<any>}} AnyMetricEndpointBuilder */
/** @template T @typedef {{Object}} SingleItemBuilder
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch the item
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<T>}} then - Thenable
*/
/**
* @template T
* @typedef {{Object}} SkippedBuilder
/** @template T @typedef {{Object}} DateSingleItemBuilder
* @property {{(onUpdate?: (value: DateMetricData<T>) => void) => Promise<DateMetricData<T>>}} fetch - Fetch the item
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{DateThenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} SkippedBuilder
* @property {{(n: number) => RangeBuilder<T>}} take - Take n items after skipped position
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch from skipped position to end
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<T>}} then - Thenable
*/
/**
* @template T
* @typedef {{Object}} RangeBuilder
/** @template T @typedef {{Object}} DateSkippedBuilder
* @property {{(n: number) => DateRangeBuilder<T>}} take - Take n items after skipped position
* @property {{(onUpdate?: (value: DateMetricData<T>) => void) => Promise<DateMetricData<T>>}} fetch - Fetch from skipped position to end
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{DateThenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} RangeBuilder
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch the range
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{Thenable<T>}} then - Thenable
*/
/** @template T @typedef {{Object}} DateRangeBuilder
* @property {{(onUpdate?: (value: DateMetricData<T>) => void) => Promise<DateMetricData<T>>}} fetch - Fetch the range
* @property {{() => Promise<string>}} fetchCsv - Fetch as CSV
* @property {{DateThenable<T>}} then - Thenable
*/
/**
* @template T
* @typedef {{Object}} MetricPattern
* @property {{string}} name - The metric name
* @property {{Readonly<Partial<Record<Index, MetricEndpointBuilder<T>>>>}} by - Index endpoints as lazy getters. Access via .by.day1 or .by['day1']
* @property {{Readonly<Partial<Record<Index, MetricEndpointBuilder<T>>>>}} by - Index endpoints as lazy getters
* @property {{() => readonly Index[]}} indexes - Get the list of available indexes
* @property {{(index: Index) => MetricEndpointBuilder<T>|undefined}} get - Get an endpoint for a specific index
*/
@@ -264,7 +300,7 @@ function _wrapMetricData(raw) {{
* @param {{BrkClientBase}} client
* @param {{string}} name - The metric vec name
* @param {{Index}} index - The index name
* @returns {{MetricEndpointBuilder<T>}}
* @returns {{DateMetricEndpointBuilder<T>}}
*/
function _endpoint(client, name, index) {{
const p = `/api/metric/${{name}}/${{index}}`;
@@ -287,7 +323,7 @@ function _endpoint(client, name, index) {{
/**
* @param {{number}} [start]
* @param {{number}} [end]
* @returns {{RangeBuilder<T>}}
* @returns {{DateRangeBuilder<T>}}
*/
const rangeBuilder = (start, end) => ({{
fetch(onUpdate) {{ return client._fetchMetricData(buildPath(start, end), onUpdate); }},
@@ -297,7 +333,7 @@ function _endpoint(client, name, index) {{
/**
* @param {{number}} idx
* @returns {{SingleItemBuilder<T>}}
* @returns {{DateSingleItemBuilder<T>}}
*/
const singleItemBuilder = (idx) => ({{
fetch(onUpdate) {{ return client._fetchMetricData(buildPath(idx, idx + 1), onUpdate); }},
@@ -307,7 +343,7 @@ function _endpoint(client, name, index) {{
/**
* @param {{number}} start
* @returns {{SkippedBuilder<T>}}
* @returns {{DateSkippedBuilder<T>}}
*/
const skippedBuilder = (start) => ({{
take(n) {{ return rangeBuilder(start, start + n); }},
@@ -316,9 +352,9 @@ function _endpoint(client, name, index) {{
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
}});
/** @type {{MetricEndpointBuilder<T>}} */
/** @type {{DateMetricEndpointBuilder<T>}} */
const endpoint = {{
get(idx) {{ return singleItemBuilder(idx); }},
get(idx) {{ if (idx instanceof Date) idx = dateToIndex(index, idx); return singleItemBuilder(idx); }},
slice(start, end) {{
if (start instanceof Date) start = dateToIndex(index, start);
if (end instanceof Date) end = dateToIndex(index, end);
@@ -345,7 +381,8 @@ class BrkClientBase {{
*/
constructor(options) {{
const isString = typeof options === 'string';
this.baseUrl = isString ? options : options.baseUrl;
const rawUrl = isString ? options : options.baseUrl;
this.baseUrl = rawUrl.endsWith('/') ? rawUrl.slice(0, -1) : rawUrl;
this.timeout = isString ? 5000 : (options.timeout ?? 5000);
/** @type {{Promise<Cache | null>}} */
this._cachePromise = _openCache(isString ? undefined : options.cache);
@@ -359,8 +396,7 @@ class BrkClientBase {{
* @returns {{Promise<Response>}}
*/
async get(path) {{
const base = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
const url = `${{base}}${{path}}`;
const url = `${{this.baseUrl}}${{path}}`;
const res = await fetch(url, {{ signal: AbortSignal.timeout(this.timeout) }});
if (!res.ok) throw new BrkError(`HTTP ${{res.status}}: ${{url}}`, res.status);
return res;
@@ -374,8 +410,7 @@ class BrkClientBase {{
* @returns {{Promise<T>}}
*/
async getJson(path, onUpdate) {{
const base = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
const url = `${{base}}${{path}}`;
const url = `${{this.baseUrl}}${{path}}`;
const cache = this._cache ?? await this._cachePromise;
let resolved = false;
@@ -437,8 +472,8 @@ class BrkClientBase {{
* Fetch metric data and wrap with helper methods (internal)
* @template T
* @param {{string}} path
* @param {{(value: MetricData<T>) => void}} [onUpdate]
* @returns {{Promise<MetricData<T>>}}
* @param {{(value: DateMetricData<T>) => void}} [onUpdate]
* @returns {{Promise<DateMetricData<T>>}}
*/
async _fetchMetricData(path, onUpdate) {{
const wrappedOnUpdate = onUpdate ? (/** @type {{MetricData<T>}} */ raw) => onUpdate(_wrapMetricData(raw)) : undefined;
@@ -566,7 +601,7 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
* @param {{readonly Index[]}} indexes - The supported indexes
*/
function _mp(client, name, indexes) {{
const by = /** @type {{any}} */ ({{}});
const by = {{}};
for (const idx of indexes) {{
Object.defineProperty(by, idx, {{
get() {{ return _endpoint(client, name, idx); }},
@@ -577,8 +612,9 @@ function _mp(client, name, indexes) {{
return {{
name,
by,
/** @returns {{readonly Index[]}} */
indexes() {{ return indexes; }},
/** @param {{Index}} index */
/** @param {{Index}} index @returns {{MetricEndpointBuilder<T>|undefined}} */
get(index) {{ return indexes.includes(index) ? _endpoint(client, name, index) : undefined; }}
}};
}}
@@ -593,10 +629,12 @@ function _mp(client, name, indexes) {{
.indexes
.iter()
.map(|idx| {
format!(
"readonly {}: MetricEndpointBuilder<T>",
idx.name()
)
let builder = if idx.is_date_based() {
"DateMetricEndpointBuilder"
} else {
"MetricEndpointBuilder"
};
format!("readonly {}: {}<T>", idx.name(), builder)
})
.collect();
let by_type = format!("{{ {} }}", by_fields.join(", "));
@@ -617,7 +655,8 @@ function _mp(client, name, indexes) {{
.unwrap();
writeln!(
output,
"function create{}(client, name) {{ return _mp(client, name, _i{}); }}",
"function create{}(client, name) {{ return /** @type {{{}<T>}} */ (_mp(client, name, _i{})); }}",
pattern.name,
pattern.name,
i + 1
)
+180 -102
View File
@@ -99,16 +99,16 @@ class BrkClientBase:
"""Make a GET request and return text."""
return self.get(path).decode()
def close(self):
def close(self) -> None:
"""Close the HTTP client."""
if self._conn:
self._conn.close()
self._conn = None
def __enter__(self):
def __enter__(self) -> BrkClientBase:
return self
def __exit__(self, exc_type, exc_val, exc_tb):
def __exit__(self, exc_type: Optional[type], exc_val: Optional[BaseException], exc_tb: Optional[Any]) -> None:
self.close()
@@ -221,7 +221,7 @@ def _date_to_index(index: str, d: Union[date, datetime]) -> int:
@dataclass
class MetricData(Generic[T]):
"""Metric data with range information."""
"""Metric data with range information. Always int-indexed."""
version: int
index: Index
total: int
@@ -235,63 +235,96 @@ class MetricData(Generic[T]):
"""Whether this metric uses a date-based index."""
return self.index in _DATE_INDEXES
def dates(self) -> list:
"""Get dates for the index range. Date-based indexes only, throws otherwise."""
return [_index_to_date(self.index, i) for i in range(self.start, self.end)]
def indexes(self) -> List[int]:
"""Get raw index numbers."""
return list(range(self.start, self.end))
def keys(self) -> list:
"""Get keys: dates for date-based indexes, index numbers otherwise."""
return self.dates() if self.is_date_based else self.indexes()
def keys(self) -> List[int]:
"""Get keys as index numbers."""
return self.indexes()
def items(self) -> list:
"""Get (key, value) pairs: keys are dates for date-based, numbers otherwise."""
return list(zip(self.keys(), self.data))
def items(self) -> List[Tuple[int, T]]:
"""Get (index, value) pairs."""
return list(zip(self.indexes(), self.data))
def to_dict(self) -> dict:
"""Return {{key: value}} dict: keys are dates for date-based, numbers otherwise."""
return dict(zip(self.keys(), self.data))
def to_dict(self) -> Dict[int, T]:
"""Return {{index: value}} dict."""
return dict(zip(self.indexes(), self.data))
def __iter__(self):
"""Iterate over (key, value) pairs. Keys are dates for date-based, numbers otherwise."""
return iter(zip(self.keys(), self.data))
def __iter__(self) -> Iterator[Tuple[int, T]]:
"""Iterate over (index, value) pairs."""
return iter(zip(self.indexes(), self.data))
def __len__(self) -> int:
return len(self.data)
def to_polars(self) -> pl.DataFrame:
"""Convert to Polars DataFrame with 'index' and 'value' columns."""
try:
import polars as pl # type: ignore[import-not-found]
except ImportError:
raise ImportError("polars is required: pip install polars")
return pl.DataFrame({{"index": self.indexes(), "value": self.data}})
def to_pandas(self) -> pd.DataFrame:
"""Convert to Pandas DataFrame with 'index' and 'value' columns."""
try:
import pandas as pd # type: ignore[import-not-found]
except ImportError:
raise ImportError("pandas is required: pip install pandas")
return pd.DataFrame({{"index": self.indexes(), "value": self.data}})
@dataclass
class DateMetricData(MetricData[T]):
"""Metric data with date-based index. Extends MetricData with date methods."""
def dates(self) -> List[Union[date, datetime]]:
"""Get dates for the index range. Returns datetime for sub-daily indexes, date for daily+."""
return [_index_to_date(self.index, i) for i in range(self.start, self.end)]
def date_items(self) -> List[Tuple[Union[date, datetime], T]]:
"""Get (date, value) pairs."""
return list(zip(self.dates(), self.data))
def to_date_dict(self) -> Dict[Union[date, datetime], T]:
"""Return {{date: value}} dict."""
return dict(zip(self.dates(), self.data))
def to_polars(self, with_dates: bool = True) -> pl.DataFrame:
"""Convert to Polars DataFrame. Requires polars to be installed.
"""Convert to Polars DataFrame.
Returns a DataFrame with columns:
- 'date' and 'value' if with_dates=True and index is date-based
- 'date' and 'value' if with_dates=True (default)
- 'index' and 'value' 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.is_date_based:
if with_dates:
return pl.DataFrame({{"date": self.dates(), "value": self.data}})
return pl.DataFrame({{"index": self.indexes(), "value": self.data}})
def to_pandas(self, with_dates: bool = True) -> pd.DataFrame:
"""Convert to Pandas DataFrame. Requires pandas to be installed.
"""Convert to Pandas DataFrame.
Returns a DataFrame with columns:
- 'date' and 'value' if with_dates=True and index is date-based
- 'date' and 'value' if with_dates=True (default)
- 'index' and 'value' 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.is_date_based:
if with_dates:
return pd.DataFrame({{"date": self.dates(), "value": self.data}})
return pd.DataFrame({{"index": self.indexes(), "value": self.data}})
# Type alias for non-generic usage
# Type aliases for non-generic usage
AnyMetricData = MetricData[Any]
AnyDateMetricData = DateMetricData[Any]
class _EndpointConfig:
@@ -325,9 +358,15 @@ class _EndpointConfig:
p = self.path()
return f"{{p}}?{{query}}" if query else p
def get_metric(self) -> MetricData:
def _new(self, start: Optional[int] = None, end: Optional[int] = None) -> _EndpointConfig:
return _EndpointConfig(self.client, self.name, self.index, start, end)
def get_metric(self) -> MetricData[Any]:
return MetricData(**self.client.get_json(self._build_path()))
def get_date_metric(self) -> DateMetricData[Any]:
return DateMetricData(**self.client.get_json(self._build_path()))
def get_csv(self) -> str:
return self.client.get_text(self._build_path(format='csv'))
@@ -371,10 +410,7 @@ class SkippedBuilder(Generic[T]):
def take(self, n: int) -> RangeBuilder[T]:
"""Take n items after the skipped position."""
start = self._config.start or 0
return RangeBuilder(_EndpointConfig(
self._config.client, self._config.name, self._config.index,
start, start + n
))
return RangeBuilder(self._config._new(start, start + n))
def fetch(self) -> MetricData[T]:
"""Fetch from skipped position to end."""
@@ -385,29 +421,35 @@ class SkippedBuilder(Generic[T]):
return self._config.get_csv()
class MetricEndpointBuilder(Generic[T]):
"""Builder for metric endpoint queries.
class DateRangeBuilder(RangeBuilder[T]):
"""Range builder that returns DateMetricData."""
def fetch(self) -> DateMetricData[T]:
return self._config.get_date_metric()
Use method chaining to specify the data range, then call fetch() or fetch_csv() to execute.
class DateSingleItemBuilder(SingleItemBuilder[T]):
"""Single item builder that returns DateMetricData."""
def fetch(self) -> DateMetricData[T]:
return self._config.get_date_metric()
class DateSkippedBuilder(SkippedBuilder[T]):
"""Skipped builder that returns DateMetricData."""
def take(self, n: int) -> DateRangeBuilder[T]:
start = self._config.start or 0
return DateRangeBuilder(self._config._new(start, start + n))
def fetch(self) -> DateMetricData[T]:
return self._config.get_date_metric()
class MetricEndpointBuilder(Generic[T]):
"""Builder for metric endpoint queries with int-based indexing.
Examples:
# Fetch all data
data = endpoint.fetch()
# Single item access
data = endpoint[5].fetch()
# Slice syntax (Python-native)
data = endpoint[:10].fetch() # First 10
data = endpoint[-5:].fetch() # Last 5
data = endpoint[100:110].fetch() # Range
# Convenience methods (pandas-style)
data = endpoint.head().fetch() # First 10 (default)
data = endpoint.head(20).fetch() # First 20
data = endpoint.tail(5).fetch() # Last 5
# Iterator-style chaining
data = endpoint[:10].fetch()
data = endpoint.head(20).fetch()
data = endpoint.skip(100).take(10).fetch()
"""
@@ -419,66 +461,30 @@ class MetricEndpointBuilder(Generic[T]):
@overload
def __getitem__(self, key: slice) -> RangeBuilder[T]: ...
def __getitem__(self, key: Union[int, slice, date, datetime]) -> Union[SingleItemBuilder[T], RangeBuilder[T]]:
"""Access single item or slice. Accepts dates for date-based indexes.
Examples:
endpoint[5] # Single item at index 5
endpoint[:10] # First 10
endpoint[-5:] # Last 5
endpoint[100:110] # Range 100-109
endpoint[date(2020, 1, 1):date(2023, 1, 1)] # Date range
endpoint[date(2020, 1, 1):] # Since date
"""
if isinstance(key, (date, datetime)):
idx = _date_to_index(self._config.index, key)
return SingleItemBuilder(_EndpointConfig(
self._config.client, self._config.name, self._config.index,
idx, idx + 1
))
def __getitem__(self, key: Union[int, slice]) -> Union[SingleItemBuilder[T], RangeBuilder[T]]:
"""Access single item or slice by integer index."""
if isinstance(key, int):
return SingleItemBuilder(_EndpointConfig(
self._config.client, self._config.name, self._config.index,
key, key + 1
))
start, stop = key.start, key.stop
if isinstance(start, (date, datetime)):
start = _date_to_index(self._config.index, start)
if isinstance(stop, (date, datetime)):
stop = _date_to_index(self._config.index, stop)
return RangeBuilder(_EndpointConfig(
self._config.client, self._config.name, self._config.index,
start, stop
))
return SingleItemBuilder(self._config._new(key, key + 1))
return RangeBuilder(self._config._new(key.start, key.stop))
def head(self, n: int = 10) -> RangeBuilder[T]:
"""Get the first n items (pandas-style)."""
return RangeBuilder(_EndpointConfig(
self._config.client, self._config.name, self._config.index,
None, n
))
"""Get the first n items."""
return RangeBuilder(self._config._new(end=n))
def tail(self, n: int = 10) -> RangeBuilder[T]:
"""Get the last n items (pandas-style)."""
start, end = (None, 0) if n == 0 else (-n, None)
return RangeBuilder(_EndpointConfig(
self._config.client, self._config.name, self._config.index,
start, end
))
"""Get the last n items."""
return RangeBuilder(self._config._new(end=0) if n == 0 else self._config._new(start=-n))
def skip(self, n: int) -> SkippedBuilder[T]:
"""Skip the first n items. Chain with take() to get a range."""
return SkippedBuilder(_EndpointConfig(
self._config.client, self._config.name, self._config.index,
n, None
))
"""Skip the first n items."""
return SkippedBuilder(self._config._new(start=n))
def fetch(self) -> MetricData[T]:
"""Fetch all data as parsed JSON."""
"""Fetch all data."""
return self._config.get_metric()
def fetch_csv(self) -> str:
"""Fetch all data as CSV string."""
"""Fetch all data as CSV."""
return self._config.get_csv()
def path(self) -> str:
@@ -486,8 +492,72 @@ class MetricEndpointBuilder(Generic[T]):
return self._config.path()
# Type alias for non-generic usage
class DateMetricEndpointBuilder(Generic[T]):
"""Builder for metric endpoint queries with date-based indexing.
Accepts dates in __getitem__ and returns DateMetricData from fetch().
Examples:
data = endpoint.fetch()
data = endpoint[date(2020, 1, 1)].fetch()
data = endpoint[date(2020, 1, 1):date(2023, 1, 1)].fetch()
data = endpoint[:10].fetch()
"""
def __init__(self, client: BrkClientBase, name: str, index: Index):
self._config = _EndpointConfig(client, name, index)
@overload
def __getitem__(self, key: int) -> DateSingleItemBuilder[T]: ...
@overload
def __getitem__(self, key: datetime) -> DateSingleItemBuilder[T]: ...
@overload
def __getitem__(self, key: date) -> DateSingleItemBuilder[T]: ...
@overload
def __getitem__(self, key: slice) -> DateRangeBuilder[T]: ...
def __getitem__(self, key: Union[int, slice, date, datetime]) -> Union[DateSingleItemBuilder[T], DateRangeBuilder[T]]:
"""Access single item or slice. Accepts int, date, or datetime."""
if isinstance(key, (date, datetime)):
idx = _date_to_index(self._config.index, key)
return DateSingleItemBuilder(self._config._new(idx, idx + 1))
if isinstance(key, int):
return DateSingleItemBuilder(self._config._new(key, key + 1))
start, stop = key.start, key.stop
if isinstance(start, (date, datetime)):
start = _date_to_index(self._config.index, start)
if isinstance(stop, (date, datetime)):
stop = _date_to_index(self._config.index, stop)
return DateRangeBuilder(self._config._new(start, stop))
def head(self, n: int = 10) -> DateRangeBuilder[T]:
"""Get the first n items."""
return DateRangeBuilder(self._config._new(end=n))
def tail(self, n: int = 10) -> DateRangeBuilder[T]:
"""Get the last n items."""
return DateRangeBuilder(self._config._new(end=0) if n == 0 else self._config._new(start=-n))
def skip(self, n: int) -> DateSkippedBuilder[T]:
"""Skip the first n items."""
return DateSkippedBuilder(self._config._new(start=n))
def fetch(self) -> DateMetricData[T]:
"""Fetch all data."""
return self._config.get_date_metric()
def fetch_csv(self) -> str:
"""Fetch all data as CSV."""
return self._config.get_csv()
def path(self) -> str:
"""Get the base endpoint path."""
return self._config.path()
# Type aliases for non-generic usage
AnyMetricEndpointBuilder = MetricEndpointBuilder[Any]
AnyDateMetricEndpointBuilder = DateMetricEndpointBuilder[Any]
class MetricPattern(Protocol[T]):
@@ -535,11 +605,14 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
}
writeln!(output).unwrap();
// Generate helper function
// Generate helper functions
writeln!(
output,
r#"def _ep(c: BrkClientBase, n: str, i: Index) -> MetricEndpointBuilder[Any]:
return MetricEndpointBuilder(c, n, i)
def _dep(c: BrkClientBase, n: str, i: Index) -> DateMetricEndpointBuilder[Any]:
return DateMetricEndpointBuilder(c, n, i)
"#
)
.unwrap();
@@ -560,10 +633,15 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
for index in &pattern.indexes {
let method_name = index_to_field_name(index);
let index_name = index.name();
let (builder_type, helper) = if index.is_date_based() {
("DateMetricEndpointBuilder", "_dep")
} else {
("MetricEndpointBuilder", "_ep")
};
writeln!(
output,
" def {}(self) -> MetricEndpointBuilder[T]: return _ep(self._c, self._n, '{}')",
method_name, index_name
" def {}(self) -> {}[T]: return {}(self._c, self._n, '{}')",
method_name, builder_type, helper, index_name
)
.unwrap();
}
@@ -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, Iterator, Tuple, TYPE_CHECKING"
"from typing import TypeVar, Generic, Any, Dict, Optional, List, Iterator, Literal, TypedDict, Union, Protocol, overload, Tuple, TYPE_CHECKING"
)
.unwrap();
writeln!(
@@ -57,6 +57,20 @@ impl BrkClient {{
index,
)
}}
/// Create a dynamic date-based metric endpoint builder.
///
/// Returns `Err` if the index is not date-based.
pub fn date_metric(&self, metric: impl Into<Metric>, index: Index) -> Result<DateMetricEndpointBuilder<serde_json::Value>> {{
if !index.is_date_based() {{
return Err(BrkError {{ message: format!("{{}} is not a date-based index", index.name()) }});
}}
Ok(DateMetricEndpointBuilder::new(
self.base.clone(),
Arc::from(metric.into().as_str()),
index,
))
}}
"#,
VERSION = VERSION
)
@@ -200,45 +200,39 @@ impl EndpointConfig {{
}}
}}
/// Initial builder for metric endpoint queries.
/// Builder for metric endpoint queries.
///
/// Use method chaining to specify the data range, then call `fetch()` or `fetch_csv()` to execute.
/// Parameterized by element type `T` and response type `D` (defaults to `MetricData<T>`).
/// For date-based indexes, use `DateMetricEndpointBuilder<T>` which sets `D = DateMetricData<T>`.
///
/// # Examples
/// ```ignore
/// // Fetch all data
/// let data = endpoint.fetch()?;
///
/// // Get single item at index 5
/// let data = endpoint.get(5).fetch()?;
///
/// // Get first 10 using range
/// let data = endpoint.range(..10).fetch()?;
///
/// // Get range [100, 200)
/// let data = endpoint.range(100..200).fetch()?;
///
/// // Get first 10 (convenience)
/// let data = endpoint.take(10).fetch()?;
///
/// // Get last 10
/// let data = endpoint.last(10).fetch()?;
///
/// // Iterator-style chaining
/// let data = endpoint.skip(100).take(10).fetch()?;
/// let data = endpoint.fetch()?; // all data
/// let data = endpoint.get(5).fetch()?; // single item
/// let data = endpoint.range(..10).fetch()?; // first 10
/// let data = endpoint.range(100..200).fetch()?; // range [100, 200)
/// let data = endpoint.take(10).fetch()?; // first 10 (convenience)
/// let data = endpoint.last(10).fetch()?; // last 10
/// let data = endpoint.skip(100).take(10).fetch()?; // iterator-style
/// ```
pub struct MetricEndpointBuilder<T> {{
pub struct MetricEndpointBuilder<T, D = MetricData<T>> {{
config: EndpointConfig,
_marker: std::marker::PhantomData<T>,
_marker: std::marker::PhantomData<fn() -> (T, D)>,
}}
impl<T: DeserializeOwned> MetricEndpointBuilder<T> {{
/// Builder for date-based metric endpoint queries.
///
/// Like `MetricEndpointBuilder` but returns `DateMetricData` and provides
/// date-based access methods (`get_date`, `date_range`).
pub type DateMetricEndpointBuilder<T> = MetricEndpointBuilder<T, DateMetricData<T>>;
impl<T: DeserializeOwned, D: DeserializeOwned> MetricEndpointBuilder<T, D> {{
pub fn new(client: Arc<BrkClientBase>, name: Arc<str>, index: Index) -> Self {{
Self {{ config: EndpointConfig::new(client, name, index), _marker: std::marker::PhantomData }}
}}
/// Select a specific index position.
pub fn get(mut self, index: usize) -> SingleItemBuilder<T> {{
pub fn get(mut self, index: usize) -> SingleItemBuilder<T, D> {{
self.config.start = Some(index as i64);
self.config.end = Some(index as i64 + 1);
SingleItemBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
@@ -252,7 +246,7 @@ impl<T: DeserializeOwned> MetricEndpointBuilder<T> {{
/// endpoint.range(100..110) // indices 100-109
/// endpoint.range(100..) // from 100 to end
/// ```
pub fn range<R: RangeBounds<usize>>(mut self, range: R) -> RangeBuilder<T> {{
pub fn range<R: RangeBounds<usize>>(mut self, range: R) -> RangeBuilder<T, D> {{
self.config.start = match range.start_bound() {{
Bound::Included(&n) => Some(n as i64),
Bound::Excluded(&n) => Some(n as i64 + 1),
@@ -267,12 +261,12 @@ impl<T: DeserializeOwned> MetricEndpointBuilder<T> {{
}}
/// Take the first n items.
pub fn take(self, n: usize) -> RangeBuilder<T> {{
pub fn take(self, n: usize) -> RangeBuilder<T, D> {{
self.range(..n)
}}
/// Take the last n items.
pub fn last(mut self, n: usize) -> RangeBuilder<T> {{
pub fn last(mut self, n: usize) -> RangeBuilder<T, D> {{
if n == 0 {{
self.config.end = Some(0);
}} else {{
@@ -282,13 +276,13 @@ impl<T: DeserializeOwned> MetricEndpointBuilder<T> {{
}}
/// Skip the first n items. Chain with `take(n)` to get a range.
pub fn skip(mut self, n: usize) -> SkippedBuilder<T> {{
pub fn skip(mut self, n: usize) -> SkippedBuilder<T, D> {{
self.config.start = Some(n as i64);
SkippedBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
}}
/// Fetch all data as parsed JSON.
pub fn fetch(self) -> Result<MetricData<T>> {{
pub fn fetch(self) -> Result<D> {{
self.config.get_json(None)
}}
@@ -303,15 +297,47 @@ impl<T: DeserializeOwned> MetricEndpointBuilder<T> {{
}}
}}
/// Builder for single item access.
pub struct SingleItemBuilder<T> {{
config: EndpointConfig,
_marker: std::marker::PhantomData<T>,
/// Date-specific methods available only on `DateMetricEndpointBuilder`.
impl<T: DeserializeOwned> MetricEndpointBuilder<T, DateMetricData<T>> {{
/// Select a specific date position (for day-precision or coarser indexes).
pub fn get_date(self, date: Date) -> SingleItemBuilder<T, DateMetricData<T>> {{
let index = self.config.index.date_to_index(date).unwrap_or(0);
self.get(index)
}}
/// Select a date range (for day-precision or coarser indexes).
pub fn date_range(self, start: Date, end: Date) -> RangeBuilder<T, DateMetricData<T>> {{
let s = self.config.index.date_to_index(start).unwrap_or(0);
let e = self.config.index.date_to_index(end).unwrap_or(0);
self.range(s..e)
}}
/// Select a specific timestamp position (works for all date-based indexes including sub-daily).
pub fn get_timestamp(self, ts: Timestamp) -> SingleItemBuilder<T, DateMetricData<T>> {{
let index = self.config.index.timestamp_to_index(ts).unwrap_or(0);
self.get(index)
}}
/// Select a timestamp range (works for all date-based indexes including sub-daily).
pub fn timestamp_range(self, start: Timestamp, end: Timestamp) -> RangeBuilder<T, DateMetricData<T>> {{
let s = self.config.index.timestamp_to_index(start).unwrap_or(0);
let e = self.config.index.timestamp_to_index(end).unwrap_or(0);
self.range(s..e)
}}
}}
impl<T: DeserializeOwned> SingleItemBuilder<T> {{
/// Builder for single item access.
pub struct SingleItemBuilder<T, D = MetricData<T>> {{
config: EndpointConfig,
_marker: std::marker::PhantomData<fn() -> (T, D)>,
}}
/// Date-aware single item builder.
pub type DateSingleItemBuilder<T> = SingleItemBuilder<T, DateMetricData<T>>;
impl<T: DeserializeOwned, D: DeserializeOwned> SingleItemBuilder<T, D> {{
/// Fetch the single item.
pub fn fetch(self) -> Result<MetricData<T>> {{
pub fn fetch(self) -> Result<D> {{
self.config.get_json(None)
}}
@@ -322,21 +348,24 @@ impl<T: DeserializeOwned> SingleItemBuilder<T> {{
}}
/// Builder after calling `skip(n)`. Chain with `take(n)` to specify count.
pub struct SkippedBuilder<T> {{
pub struct SkippedBuilder<T, D = MetricData<T>> {{
config: EndpointConfig,
_marker: std::marker::PhantomData<T>,
_marker: std::marker::PhantomData<fn() -> (T, D)>,
}}
impl<T: DeserializeOwned> SkippedBuilder<T> {{
/// Date-aware skipped builder.
pub type DateSkippedBuilder<T> = SkippedBuilder<T, DateMetricData<T>>;
impl<T: DeserializeOwned, D: DeserializeOwned> SkippedBuilder<T, D> {{
/// Take n items after the skipped position.
pub fn take(mut self, n: usize) -> RangeBuilder<T> {{
pub fn take(mut self, n: usize) -> RangeBuilder<T, D> {{
let start = self.config.start.unwrap_or(0);
self.config.end = Some(start + n as i64);
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
}}
/// Fetch from the skipped position to the end.
pub fn fetch(self) -> Result<MetricData<T>> {{
pub fn fetch(self) -> Result<D> {{
self.config.get_json(None)
}}
@@ -347,14 +376,17 @@ impl<T: DeserializeOwned> SkippedBuilder<T> {{
}}
/// Builder with range fully specified.
pub struct RangeBuilder<T> {{
pub struct RangeBuilder<T, D = MetricData<T>> {{
config: EndpointConfig,
_marker: std::marker::PhantomData<T>,
_marker: std::marker::PhantomData<fn() -> (T, D)>,
}}
impl<T: DeserializeOwned> RangeBuilder<T> {{
/// Date-aware range builder.
pub type DateRangeBuilder<T> = RangeBuilder<T, DateMetricData<T>>;
impl<T: DeserializeOwned, D: DeserializeOwned> RangeBuilder<T, D> {{
/// Fetch the range as parsed JSON.
pub fn fetch(self) -> Result<MetricData<T>> {{
pub fn fetch(self) -> Result<D> {{
self.config.get_json(None)
}}
@@ -389,13 +421,18 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
}
writeln!(output).unwrap();
// Generate helper function
// Generate helper functions
writeln!(
output,
r#"#[inline]
fn _ep<T: DeserializeOwned>(c: &Arc<BrkClientBase>, n: &Arc<str>, i: Index) -> MetricEndpointBuilder<T> {{
MetricEndpointBuilder::new(c.clone(), n.clone(), i)
}}
#[inline]
fn _dep<T: DeserializeOwned>(c: &Arc<BrkClientBase>, n: &Arc<str>, i: Index) -> DateMetricEndpointBuilder<T> {{
DateMetricEndpointBuilder::new(c.clone(), n.clone(), i)
}}
"#
)
.unwrap();
@@ -412,12 +449,21 @@ fn _ep<T: DeserializeOwned>(c: &Arc<BrkClientBase>, n: &Arc<str>, i: Index) -> M
writeln!(output, "impl<T: DeserializeOwned> {}<T> {{", by_name).unwrap();
for index in &pattern.indexes {
let method_name = index_to_field_name(index);
writeln!(
output,
" pub fn {}(&self) -> MetricEndpointBuilder<T> {{ _ep(&self.client, &self.name, Index::{}) }}",
method_name, index
)
.unwrap();
if index.is_date_based() {
writeln!(
output,
" pub fn {}(&self) -> DateMetricEndpointBuilder<T> {{ _dep(&self.client, &self.name, Index::{}) }}",
method_name, index
)
.unwrap();
} else {
writeln!(
output,
" pub fn {}(&self) -> MetricEndpointBuilder<T> {{ _ep(&self.client, &self.name, Index::{}) }}",
method_name, index
)
.unwrap();
}
}
writeln!(output, "}}\n").unwrap();