mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-11 11:08:13 -07:00
global: snapshot
This commit is contained in:
@@ -458,4 +458,33 @@ mod tests {
|
||||
Some(&FieldNamePosition::Append("_cumulative".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_analyze_pattern_level_no_base_field() {
|
||||
// When there's no base field (like block_weight which has no block_weight metric),
|
||||
// only suffixed metrics like block_weight_average, block_weight_sum, etc.
|
||||
// Base should still be "block_weight"
|
||||
let children = vec![
|
||||
("average".to_string(), "block_weight_average".to_string()),
|
||||
("sum".to_string(), "block_weight_sum".to_string()),
|
||||
(
|
||||
"cumulative".to_string(),
|
||||
"block_weight_cumulative".to_string(),
|
||||
),
|
||||
("max".to_string(), "block_weight_max".to_string()),
|
||||
("min".to_string(), "block_weight_min".to_string()),
|
||||
];
|
||||
let analysis = analyze_pattern_level(&children);
|
||||
|
||||
assert!(matches!(analysis.common, CommonDenominator::Prefix(_)));
|
||||
assert_eq!(analysis.base, "block_weight");
|
||||
assert_eq!(
|
||||
analysis.field_positions.get("average"),
|
||||
Some(&FieldNamePosition::Append("_average".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
analysis.field_positions.get("sum"),
|
||||
Some(&FieldNamePosition::Append("_sum".to_string()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
|
||||
use brk_types::{Index, TreeNode, extract_json_type};
|
||||
|
||||
use crate::{IndexSetPattern, PatternField, analysis::names::analyze_pattern_level, child_type_name};
|
||||
use crate::{IndexSetPattern, PatternField, analysis::names::{analyze_pattern_level, CommonDenominator}, child_type_name};
|
||||
|
||||
/// Get the first leaf name from a tree node.
|
||||
pub fn get_first_leaf_name(node: &TreeNode) -> Option<String> {
|
||||
@@ -124,16 +124,81 @@ fn collect_indexes_from_tree(
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of analyzing a pattern instance's base.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PatternBaseResult {
|
||||
/// The computed base name for the pattern.
|
||||
pub base: String,
|
||||
/// Whether an outlier child was excluded to find the pattern.
|
||||
/// If true, pattern factory should not be used.
|
||||
pub has_outlier: bool,
|
||||
}
|
||||
|
||||
impl PatternBaseResult {
|
||||
/// Returns true if an inline type should be generated instead of using a pattern factory.
|
||||
///
|
||||
/// This is the case when:
|
||||
/// - The child fields don't match a parameterizable pattern, OR
|
||||
/// - An outlier was detected during pattern analysis
|
||||
pub fn should_inline(&self, is_parameterizable: bool) -> bool {
|
||||
!is_parameterizable || self.has_outlier
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the metric base for a pattern instance by analyzing direct children.
|
||||
///
|
||||
/// Uses field names and first leaf names from direct children to determine
|
||||
/// the common base via `analyze_pattern_level`.
|
||||
pub fn get_pattern_instance_base(node: &TreeNode) -> String {
|
||||
///
|
||||
/// If the initial analysis fails to find a common pattern, it tries excluding
|
||||
/// each child one at a time to detect outliers (e.g., a mismatched "base" field
|
||||
/// from indexer/computed tree merging).
|
||||
///
|
||||
/// Returns both the base and whether an outlier was detected.
|
||||
pub fn get_pattern_instance_base(node: &TreeNode) -> PatternBaseResult {
|
||||
let child_names = get_direct_children_for_analysis(node);
|
||||
if child_names.is_empty() {
|
||||
return String::new();
|
||||
return PatternBaseResult {
|
||||
base: String::new(),
|
||||
has_outlier: false,
|
||||
};
|
||||
}
|
||||
|
||||
let analysis = analyze_pattern_level(&child_names);
|
||||
|
||||
// If we found a common pattern, use it
|
||||
if !matches!(analysis.common, CommonDenominator::None) {
|
||||
return PatternBaseResult {
|
||||
base: analysis.base,
|
||||
has_outlier: false,
|
||||
};
|
||||
}
|
||||
|
||||
// If no common pattern found, try excluding each child one at a time
|
||||
// to detect if there's a single outlier breaking the pattern.
|
||||
if child_names.len() > 2 {
|
||||
for i in 0..child_names.len() {
|
||||
let filtered: Vec<_> = child_names
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(j, _)| *j != i)
|
||||
.map(|(_, v)| v.clone())
|
||||
.collect();
|
||||
|
||||
let filtered_analysis = analyze_pattern_level(&filtered);
|
||||
if !matches!(filtered_analysis.common, CommonDenominator::None) {
|
||||
return PatternBaseResult {
|
||||
base: filtered_analysis.base,
|
||||
has_outlier: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PatternBaseResult {
|
||||
base: analysis.base,
|
||||
has_outlier: false,
|
||||
}
|
||||
analyze_pattern_level(&child_names).base
|
||||
}
|
||||
|
||||
/// Get (field_name, shortest_leaf_name) pairs for direct children of a branch node.
|
||||
@@ -217,3 +282,93 @@ pub fn get_fields_with_child_info(
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use brk_types::{MetricLeaf, MetricLeafWithSchema, TreeNode};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn make_leaf(name: &str) -> TreeNode {
|
||||
let leaf = MetricLeaf {
|
||||
name: name.to_string(),
|
||||
kind: "TestType".to_string(),
|
||||
indexes: BTreeSet::new(),
|
||||
};
|
||||
TreeNode::Leaf(MetricLeafWithSchema::new(leaf, serde_json::json!({})))
|
||||
}
|
||||
|
||||
fn make_branch(children: Vec<(&str, TreeNode)>) -> TreeNode {
|
||||
let map: BTreeMap<String, TreeNode> = children
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_string(), v))
|
||||
.collect();
|
||||
TreeNode::Branch(map)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_pattern_instance_base_with_base_field() {
|
||||
// Simulates vbytes tree: has base field with block_vbytes leaf
|
||||
let tree = make_branch(vec![
|
||||
("base", make_branch(vec![("dateindex", make_leaf("block_vbytes"))])),
|
||||
("average", make_branch(vec![("dateindex", make_leaf("block_vbytes_average"))])),
|
||||
("sum", make_branch(vec![("dateindex", make_leaf("block_vbytes_sum"))])),
|
||||
]);
|
||||
|
||||
let result = get_pattern_instance_base(&tree);
|
||||
assert_eq!(result.base, "block_vbytes");
|
||||
assert!(!result.has_outlier);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_pattern_instance_base_without_base_field() {
|
||||
// Simulates weight tree: NO base field, only suffixed metrics
|
||||
let tree = make_branch(vec![
|
||||
("average", make_branch(vec![("dateindex", make_leaf("block_weight_average"))])),
|
||||
("sum", make_branch(vec![("dateindex", make_leaf("block_weight_sum"))])),
|
||||
("cumulative", make_branch(vec![("dateindex", make_leaf("block_weight_cumulative"))])),
|
||||
("max", make_branch(vec![("dateindex", make_leaf("block_weight_max"))])),
|
||||
("min", make_branch(vec![("dateindex", make_leaf("block_weight_min"))])),
|
||||
]);
|
||||
|
||||
let result = get_pattern_instance_base(&tree);
|
||||
assert_eq!(result.base, "block_weight");
|
||||
assert!(!result.has_outlier);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_pattern_instance_base_with_duplicate_base_field() {
|
||||
// What if there's a "base" field that points to the same leaf as "average"?
|
||||
// This could happen if the tree generation creates a base field that shares leaves with average
|
||||
let tree = make_branch(vec![
|
||||
("base", make_branch(vec![("dateindex", make_leaf("block_weight_average"))])),
|
||||
("average", make_branch(vec![("dateindex", make_leaf("block_weight_average"))])),
|
||||
("sum", make_branch(vec![("dateindex", make_leaf("block_weight_sum"))])),
|
||||
]);
|
||||
|
||||
let result = get_pattern_instance_base(&tree);
|
||||
// Common prefix among all children is "block_weight_"
|
||||
assert_eq!(result.base, "block_weight");
|
||||
assert!(!result.has_outlier);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_pattern_instance_base_with_mismatched_base_name() {
|
||||
// Simulates the actual bug: indexed tree's "base" field has name "weight"
|
||||
// but computed tree's derived metrics use "block_weight_*" prefix.
|
||||
// After tree merge, we get a base field with mismatched naming.
|
||||
let tree = make_branch(vec![
|
||||
("base", make_leaf("weight")), // Outlier - doesn't match pattern
|
||||
("average", make_leaf("block_weight_average")),
|
||||
("sum", make_leaf("block_weight_sum")),
|
||||
("cumulative", make_leaf("block_weight_cumulative")),
|
||||
("max", make_leaf("block_weight_max")),
|
||||
("min", make_leaf("block_weight_min")),
|
||||
]);
|
||||
|
||||
let result = get_pattern_instance_base(&tree);
|
||||
// Should detect "weight" as outlier and find common prefix from others
|
||||
assert_eq!(result.base, "block_weight");
|
||||
assert!(result.has_outlier); // Pattern factory should NOT be used
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,35 @@ use std::collections::{HashMap, HashSet};
|
||||
|
||||
use brk_types::TreeNode;
|
||||
|
||||
use crate::{ClientMetadata, PatternField, get_fields_with_child_info};
|
||||
use crate::{
|
||||
ClientMetadata, PatternBaseResult, PatternField, child_type_name, get_fields_with_child_info,
|
||||
get_pattern_instance_base,
|
||||
};
|
||||
|
||||
/// Pre-computed context for a single child node.
|
||||
pub struct ChildContext<'a> {
|
||||
/// The child's field name in the tree.
|
||||
pub name: &'a str,
|
||||
/// The child node.
|
||||
pub node: &'a TreeNode,
|
||||
/// The field info for this child.
|
||||
pub field: PatternField,
|
||||
/// Child fields if this is a branch (for pattern lookup).
|
||||
pub child_fields: Option<Vec<PatternField>>,
|
||||
/// Pattern analysis result.
|
||||
pub base_result: PatternBaseResult,
|
||||
/// Whether this is a leaf node.
|
||||
pub is_leaf: bool,
|
||||
/// Whether to use an inline type instead of a pattern factory (only meaningful for branches).
|
||||
pub should_inline: bool,
|
||||
/// The type name to use for inline branches.
|
||||
pub inline_type_name: String,
|
||||
}
|
||||
|
||||
/// Context for generating a tree node, returned by `prepare_tree_node`.
|
||||
pub struct TreeNodeContext<'a> {
|
||||
/// The children of the branch node.
|
||||
pub children: &'a std::collections::BTreeMap<String, TreeNode>,
|
||||
/// Fields with optional child field info for generic pattern lookup.
|
||||
pub fields_with_child_info: Vec<(PatternField, Option<Vec<PatternField>>)>,
|
||||
/// Just the fields (for pattern lookup).
|
||||
pub fields: Vec<PatternField>,
|
||||
/// Pre-computed context for each child.
|
||||
pub children: Vec<ChildContext<'a>>,
|
||||
}
|
||||
|
||||
/// Prepare a tree node for generation.
|
||||
@@ -26,20 +45,22 @@ pub fn prepare_tree_node<'a>(
|
||||
metadata: &ClientMetadata,
|
||||
generated: &mut HashSet<String>,
|
||||
) -> Option<TreeNodeContext<'a>> {
|
||||
let TreeNode::Branch(children) = node else {
|
||||
let TreeNode::Branch(branch_children) = node else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let fields_with_child_info = get_fields_with_child_info(children, name, pattern_lookup);
|
||||
let fields_with_child_info = get_fields_with_child_info(branch_children, name, pattern_lookup);
|
||||
let fields: Vec<PatternField> = fields_with_child_info
|
||||
.iter()
|
||||
.map(|(f, _)| f.clone())
|
||||
.collect();
|
||||
|
||||
// Skip if this matches a parameterizable pattern
|
||||
// Skip if this matches a parameterizable pattern AND has no outlier
|
||||
let base_result = get_pattern_instance_base(node);
|
||||
if let Some(pattern_name) = pattern_lookup.get(&fields)
|
||||
&& pattern_name != name
|
||||
&& metadata.is_parameterizable(pattern_name)
|
||||
&& !base_result.has_outlier
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -50,9 +71,38 @@ pub fn prepare_tree_node<'a>(
|
||||
}
|
||||
generated.insert(name.to_string());
|
||||
|
||||
Some(TreeNodeContext {
|
||||
children,
|
||||
fields_with_child_info,
|
||||
fields,
|
||||
})
|
||||
// Build child contexts with pre-computed decisions
|
||||
let children: Vec<ChildContext<'a>> = branch_children
|
||||
.iter()
|
||||
.zip(fields_with_child_info)
|
||||
.map(|((child_name, child_node), (field, child_fields))| {
|
||||
let is_leaf = matches!(child_node, TreeNode::Leaf(_));
|
||||
let base_result = get_pattern_instance_base(child_node);
|
||||
let is_parameterizable = child_fields
|
||||
.as_ref()
|
||||
.is_some_and(|cf| metadata.is_parameterizable_fields(cf));
|
||||
// should_inline is only meaningful for branches
|
||||
let should_inline = !is_leaf && base_result.should_inline(is_parameterizable);
|
||||
|
||||
// Inline type name (only used when should_inline is true)
|
||||
let inline_type_name = if should_inline {
|
||||
child_type_name(name, child_name)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
ChildContext {
|
||||
name: child_name,
|
||||
node: child_node,
|
||||
field,
|
||||
child_fields,
|
||||
base_result,
|
||||
is_leaf,
|
||||
should_inline,
|
||||
inline_type_name,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Some(TreeNodeContext { children })
|
||||
}
|
||||
|
||||
@@ -63,57 +63,65 @@ class BrkError extends Error {{
|
||||
* @property {{number}} end - End index (exclusive)
|
||||
* @property {{T[]}} data - The metric data
|
||||
*/
|
||||
/** @typedef {{MetricData<unknown>}} AnyMetricData */
|
||||
/** @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
|
||||
*/
|
||||
|
||||
/**
|
||||
* Metric endpoint builder. Callable (returns itself) so both .by.dateindex and .by.dateindex() work.
|
||||
* @template T
|
||||
* @typedef {{Object}} MetricEndpointBuilder
|
||||
* @property {{(n: number) => RangeBuilder<T>}} first - Fetch first n data points
|
||||
* @property {{(n: number) => RangeBuilder<T>}} last - Fetch last n data points
|
||||
* @property {{(start: number, end: number) => RangeBuilder<T>}} range - Set explicit range [start, end)
|
||||
* @property {{(start: number) => FromBuilder<T>}} from - Set start position, chain with take() or to()
|
||||
* @property {{(end: number) => ToBuilder<T>}} to - Set end position, chain with takeLast() or from()
|
||||
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} json - Execute and return JSON (all data)
|
||||
* @property {{() => Promise<string>}} csv - Execute and return CSV (all data)
|
||||
* @property {{(index: number) => SingleItemBuilder<T>}} get - Get single item at index
|
||||
* @property {{(start?: number, end?: number) => RangeBuilder<T>}} slice - Slice like Array.slice
|
||||
* @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()
|
||||
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} fetch - Fetch all data
|
||||
* @property {{() => Promise<string>}} fetchCsv - Fetch all data as CSV
|
||||
* @property {{Thenable<T>}} then - Thenable (await endpoint)
|
||||
* @property {{string}} path - The endpoint path
|
||||
*/
|
||||
/** @typedef {{MetricEndpointBuilder<unknown>}} AnyMetricEndpointBuilder */
|
||||
/** @typedef {{MetricEndpointBuilder<any>}} AnyMetricEndpointBuilder */
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {{Object}} FromBuilder
|
||||
* @property {{(n: number) => RangeBuilder<T>}} take - Take n items from start position
|
||||
* @property {{(end: number) => RangeBuilder<T>}} to - Set end position
|
||||
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} json - Execute and return JSON
|
||||
* @property {{() => Promise<string>}} csv - Execute and return CSV
|
||||
* @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}} ToBuilder
|
||||
* @property {{(n: number) => RangeBuilder<T>}} takeLast - Take last n items before end position
|
||||
* @property {{(start: number) => RangeBuilder<T>}} from - Set start position
|
||||
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} json - Execute and return JSON
|
||||
* @property {{() => Promise<string>}} csv - Execute and return CSV
|
||||
* @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
|
||||
* @property {{(onUpdate?: (value: MetricData<T>) => void) => Promise<MetricData<T>>}} json - Execute and return JSON
|
||||
* @property {{() => Promise<string>}} csv - Execute and return CSV
|
||||
* @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}} MetricPattern
|
||||
* @property {{string}} name - The metric name
|
||||
* @property {{Partial<Record<Index, MetricEndpointBuilder<T>>>}} by - Index endpoints (lazy getters)
|
||||
* @property {{Readonly<Partial<Record<Index, MetricEndpointBuilder<T>>>>}} by - Index endpoints as lazy getters. Access via .by.dateindex or .by['dateindex']
|
||||
* @property {{() => Index[]}} indexes - Get the list of available indexes
|
||||
* @property {{(index: Index) => MetricEndpointBuilder<T>|undefined}} get - Get an endpoint for a specific index
|
||||
*/
|
||||
|
||||
/** @typedef {{MetricPattern<unknown>}} AnyMetricPattern */
|
||||
/** @typedef {{MetricPattern<any>}} AnyMetricPattern */
|
||||
|
||||
/**
|
||||
* Create a metric endpoint builder with typestate pattern.
|
||||
@@ -147,50 +155,46 @@ function _endpoint(client, name, index) {{
|
||||
* @returns {{RangeBuilder<T>}}
|
||||
*/
|
||||
const rangeBuilder = (start, end) => ({{
|
||||
json(/** @type {{((value: MetricData<T>) => void) | undefined}} */ onUpdate) {{
|
||||
return client.getJson(buildPath(start, end), onUpdate);
|
||||
}},
|
||||
csv() {{ return client.getText(buildPath(start, end, 'csv')); }},
|
||||
fetch(onUpdate) {{ return client.getJson(buildPath(start, end), onUpdate); }},
|
||||
fetchCsv() {{ return client.getText(buildPath(start, end, 'csv')); }},
|
||||
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
|
||||
}});
|
||||
|
||||
/**
|
||||
* @param {{number}} index
|
||||
* @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')); }},
|
||||
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
|
||||
}});
|
||||
|
||||
/**
|
||||
* @param {{number}} start
|
||||
* @returns {{FromBuilder<T>}}
|
||||
* @returns {{SkippedBuilder<T>}}
|
||||
*/
|
||||
const fromBuilder = (start) => ({{
|
||||
take(/** @type {{number}} */ n) {{ return rangeBuilder(start, start + n); }},
|
||||
to(/** @type {{number}} */ end) {{ return rangeBuilder(start, end); }},
|
||||
json(/** @type {{((value: MetricData<T>) => void) | undefined}} */ onUpdate) {{
|
||||
return client.getJson(buildPath(start, undefined), onUpdate);
|
||||
}},
|
||||
csv() {{ return client.getText(buildPath(start, undefined, 'csv')); }},
|
||||
const skippedBuilder = (start) => ({{
|
||||
take(n) {{ return rangeBuilder(start, start + n); }},
|
||||
fetch(onUpdate) {{ return client.getJson(buildPath(start, undefined), onUpdate); }},
|
||||
fetchCsv() {{ return client.getText(buildPath(start, undefined, 'csv')); }},
|
||||
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
|
||||
}});
|
||||
|
||||
/**
|
||||
* @param {{number}} end
|
||||
* @returns {{ToBuilder<T>}}
|
||||
*/
|
||||
const toBuilder = (end) => ({{
|
||||
takeLast(/** @type {{number}} */ n) {{ return rangeBuilder(end - n, end); }},
|
||||
from(/** @type {{number}} */ start) {{ return rangeBuilder(start, end); }},
|
||||
json(/** @type {{((value: MetricData<T>) => void) | undefined}} */ onUpdate) {{
|
||||
return client.getJson(buildPath(undefined, end), onUpdate);
|
||||
}},
|
||||
csv() {{ return client.getText(buildPath(undefined, end, 'csv')); }},
|
||||
}});
|
||||
|
||||
return {{
|
||||
first(/** @type {{number}} */ n) {{ return rangeBuilder(undefined, n); }},
|
||||
last(/** @type {{number}} */ n) {{ return rangeBuilder(-n, undefined); }},
|
||||
range(/** @type {{number}} */ start, /** @type {{number}} */ end) {{ return rangeBuilder(start, end); }},
|
||||
from(/** @type {{number}} */ start) {{ return fromBuilder(start); }},
|
||||
to(/** @type {{number}} */ end) {{ return toBuilder(end); }},
|
||||
json(/** @type {{((value: MetricData<T>) => void) | undefined}} */ onUpdate) {{
|
||||
return client.getJson(buildPath(), onUpdate);
|
||||
}},
|
||||
csv() {{ return client.getText(buildPath(undefined, undefined, 'csv')); }},
|
||||
/** @type {{MetricEndpointBuilder<T>}} */
|
||||
const endpoint = {{
|
||||
get(index) {{ return singleItemBuilder(index); }},
|
||||
slice(start, end) {{ return rangeBuilder(start, end); }},
|
||||
first(n) {{ return rangeBuilder(undefined, n); }},
|
||||
last(n) {{ return rangeBuilder(-n, undefined); }},
|
||||
skip(n) {{ return skippedBuilder(n); }},
|
||||
fetch(onUpdate) {{ return client.getJson(buildPath(), onUpdate); }},
|
||||
fetchCsv() {{ return client.getText(buildPath(undefined, undefined, 'csv')); }},
|
||||
then(resolve, reject) {{ return this.fetch().then(resolve, reject); }},
|
||||
get path() {{ return p; }},
|
||||
}};
|
||||
|
||||
return endpoint;
|
||||
}}
|
||||
|
||||
/**
|
||||
@@ -235,7 +239,7 @@ class BrkClientBase {{
|
||||
const cachedJson = cachedRes ? await cachedRes.json() : null;
|
||||
|
||||
if (cachedJson) onUpdate?.(cachedJson);
|
||||
if (!globalThis.navigator?.onLine) {{
|
||||
if (globalThis.navigator?.onLine === false) {{
|
||||
if (cachedJson) return cachedJson;
|
||||
throw new BrkError('Offline and no cached data available');
|
||||
}}
|
||||
@@ -305,7 +309,11 @@ pub fn generate_static_constants(output: &mut String) {
|
||||
fn instance_const_camel<T: Serialize>(output: &mut String, name: &str, value: &T) {
|
||||
let json_value: Value = serde_json::to_value(value).unwrap();
|
||||
let camel_value = camel_case_top_level_keys(json_value);
|
||||
write_static_const(output, name, &serde_json::to_string_pretty(&camel_value).unwrap());
|
||||
write_static_const(
|
||||
output,
|
||||
name,
|
||||
&serde_json::to_string_pretty(&camel_value).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
instance_const_camel(output, "TERM_NAMES", &TERM_NAMES);
|
||||
@@ -336,13 +344,25 @@ fn camel_case_top_level_keys(value: Value) -> Value {
|
||||
fn indent_json_const(json: &str) -> String {
|
||||
json.lines()
|
||||
.enumerate()
|
||||
.map(|(i, line)| if i == 0 { line.to_string() } else { format!(" {}", line) })
|
||||
.map(|(i, line)| {
|
||||
if i == 0 {
|
||||
line.to_string()
|
||||
} else {
|
||||
format!(" {}", line)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn write_static_const(output: &mut String, name: &str, json: &str) {
|
||||
writeln!(output, " {} = /** @type {{const}} */ ({});\n", name, indent_json_const(json)).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" {} = /** @type {{const}} */ ({});\n",
|
||||
name,
|
||||
indent_json_const(json)
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Generate index accessor factory functions.
|
||||
@@ -354,14 +374,30 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
|
||||
writeln!(output, "// Index accessor factory functions\n").unwrap();
|
||||
|
||||
for pattern in patterns {
|
||||
// Use 'readonly' to indicate these are getters (lazy evaluation)
|
||||
let by_fields: Vec<String> = pattern
|
||||
.indexes
|
||||
.iter()
|
||||
.map(|idx| format!("{}: MetricEndpointBuilder<T>", idx.serialize_long()))
|
||||
.map(|idx| {
|
||||
format!(
|
||||
"readonly {}: MetricEndpointBuilder<T>",
|
||||
idx.serialize_long()
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let by_type = format!("{{ {} }}", by_fields.join(", "));
|
||||
|
||||
writeln!(output, "/**").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" * Metric pattern with index endpoints as lazy getters."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" * Access via property (.by.dateindex) or bracket notation (.by['dateindex'])."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " * @template T").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
@@ -385,7 +421,11 @@ pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern
|
||||
|
||||
for (i, index) in pattern.indexes.iter().enumerate() {
|
||||
let index_name = index.serialize_long();
|
||||
let comma = if i < pattern.indexes.len() - 1 { "," } else { "" };
|
||||
let comma = if i < pattern.indexes.len() - 1 {
|
||||
","
|
||||
} else {
|
||||
""
|
||||
};
|
||||
writeln!(
|
||||
output,
|
||||
" get {}() {{ return _endpoint(client, name, '{}'); }}{}",
|
||||
@@ -438,8 +478,12 @@ pub fn generate_structural_patterns(
|
||||
}
|
||||
writeln!(output, " * @typedef {{Object}} {}", pattern.name).unwrap();
|
||||
for field in &pattern.fields {
|
||||
let js_type =
|
||||
metadata.field_type_annotation(field, pattern.is_generic, None, GenericSyntax::JAVASCRIPT);
|
||||
let js_type = metadata.field_type_annotation(
|
||||
field,
|
||||
pattern.is_generic,
|
||||
None,
|
||||
GenericSyntax::JAVASCRIPT,
|
||||
);
|
||||
writeln!(
|
||||
output,
|
||||
" * @property {{{}}} {}",
|
||||
@@ -469,8 +513,17 @@ pub fn generate_structural_patterns(
|
||||
writeln!(output, " * @returns {{{}}}", return_type).unwrap();
|
||||
writeln!(output, " */").unwrap();
|
||||
|
||||
let param_name = if is_parameterizable { "acc" } else { "basePath" };
|
||||
writeln!(output, "function create{}(client, {}) {{", pattern.name, param_name).unwrap();
|
||||
let param_name = if is_parameterizable {
|
||||
"acc"
|
||||
} else {
|
||||
"basePath"
|
||||
};
|
||||
writeln!(
|
||||
output,
|
||||
"function create{}(client, {}) {{",
|
||||
pattern.name, param_name
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " return {{").unwrap();
|
||||
|
||||
let syntax = JavaScriptSyntax;
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::fmt::Write;
|
||||
use brk_types::TreeNode;
|
||||
|
||||
use crate::{
|
||||
ClientMetadata, Endpoint, GenericSyntax, JavaScriptSyntax, PatternField, child_type_name,
|
||||
ClientMetadata, Endpoint, GenericSyntax, JavaScriptSyntax, PatternField,
|
||||
generate_leaf_field, get_first_leaf_name, get_node_fields, get_pattern_instance_base,
|
||||
infer_accumulated_name, prepare_tree_node, to_camel_case,
|
||||
};
|
||||
@@ -45,43 +45,41 @@ fn generate_tree_typedef(
|
||||
writeln!(output, "/**").unwrap();
|
||||
writeln!(output, " * @typedef {{Object}} {}", name).unwrap();
|
||||
|
||||
for ((field, child_fields), (child_name, _)) in
|
||||
ctx.fields_with_child_info.iter().zip(ctx.children.iter())
|
||||
{
|
||||
let js_type = metadata.resolve_tree_field_type(
|
||||
field,
|
||||
child_fields.as_deref(),
|
||||
name,
|
||||
child_name,
|
||||
GenericSyntax::JAVASCRIPT,
|
||||
);
|
||||
for child in &ctx.children {
|
||||
let js_type = if child.should_inline {
|
||||
child.inline_type_name.clone()
|
||||
} else {
|
||||
metadata.resolve_tree_field_type(
|
||||
&child.field,
|
||||
child.child_fields.as_deref(),
|
||||
name,
|
||||
child.name,
|
||||
GenericSyntax::JAVASCRIPT,
|
||||
)
|
||||
};
|
||||
|
||||
writeln!(
|
||||
output,
|
||||
" * @property {{{}}} {}",
|
||||
js_type,
|
||||
to_camel_case(&field.name)
|
||||
to_camel_case(&child.field.name)
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(output, " */\n").unwrap();
|
||||
|
||||
for (child_name, child_node) in ctx.children {
|
||||
if let TreeNode::Branch(grandchildren) = child_node {
|
||||
let child_fields = get_node_fields(grandchildren, pattern_lookup);
|
||||
// Generate typedef if no pattern match OR pattern is not parameterizable
|
||||
if !metadata.is_parameterizable_fields(&child_fields) {
|
||||
let child_type = child_type_name(name, child_name);
|
||||
generate_tree_typedef(
|
||||
output,
|
||||
&child_type,
|
||||
child_node,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
generated,
|
||||
);
|
||||
}
|
||||
// Generate child typedefs
|
||||
for child in &ctx.children {
|
||||
if child.should_inline {
|
||||
generate_tree_typedef(
|
||||
output,
|
||||
&child.inline_type_name,
|
||||
child.node,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
generated,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,12 +180,14 @@ fn generate_tree_initializer(
|
||||
.get(&child_fields)
|
||||
.filter(|name| metadata.is_parameterizable(name));
|
||||
|
||||
if let Some(pattern_name) = pattern_name {
|
||||
let arg = get_pattern_instance_base(child_node);
|
||||
let base_result = get_pattern_instance_base(child_node);
|
||||
|
||||
// Use pattern factory only if no outlier was detected
|
||||
if let Some(pattern_name) = pattern_name.filter(|_| !base_result.has_outlier) {
|
||||
writeln!(
|
||||
output,
|
||||
"{}{}: create{}(this, '{}'),",
|
||||
indent_str, field_name, pattern_name, arg
|
||||
indent_str, field_name, pattern_name, base_result.base
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
|
||||
@@ -195,143 +195,138 @@ class _EndpointConfig:
|
||||
|
||||
|
||||
class RangeBuilder(Generic[T]):
|
||||
"""Final builder with range fully specified. Can only call json() or csv()."""
|
||||
"""Builder with range specified."""
|
||||
|
||||
def __init__(self, config: _EndpointConfig):
|
||||
self._config = config
|
||||
|
||||
def json(self) -> MetricData[T]:
|
||||
"""Execute the query and return parsed JSON data."""
|
||||
def fetch(self) -> MetricData[T]:
|
||||
"""Fetch the range as parsed JSON."""
|
||||
return self._config.get_json()
|
||||
|
||||
def csv(self) -> str:
|
||||
"""Execute the query and return CSV data as a string."""
|
||||
def fetch_csv(self) -> str:
|
||||
"""Fetch the range as CSV string."""
|
||||
return self._config.get_csv()
|
||||
|
||||
|
||||
class FromBuilder(Generic[T]):
|
||||
"""Builder after calling from(start). Can chain with take() or to()."""
|
||||
class SingleItemBuilder(Generic[T]):
|
||||
"""Builder for single item access."""
|
||||
|
||||
def __init__(self, config: _EndpointConfig):
|
||||
self._config = config
|
||||
|
||||
def fetch(self) -> MetricData[T]:
|
||||
"""Fetch the single item."""
|
||||
return self._config.get_json()
|
||||
|
||||
def fetch_csv(self) -> str:
|
||||
"""Fetch as CSV."""
|
||||
return self._config.get_csv()
|
||||
|
||||
|
||||
class SkippedBuilder(Generic[T]):
|
||||
"""Builder after calling skip(n). Chain with take() to specify count."""
|
||||
|
||||
def __init__(self, config: _EndpointConfig):
|
||||
self._config = config
|
||||
|
||||
def take(self, n: int) -> RangeBuilder[T]:
|
||||
"""Take n items from the start position."""
|
||||
"""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
|
||||
))
|
||||
|
||||
def to(self, end: int) -> RangeBuilder[T]:
|
||||
"""Set the end position."""
|
||||
return RangeBuilder(_EndpointConfig(
|
||||
self._config.client, self._config.name, self._config.index,
|
||||
self._config.start, end
|
||||
))
|
||||
|
||||
def json(self) -> MetricData[T]:
|
||||
"""Execute the query and return parsed JSON data (from start to end of data)."""
|
||||
def fetch(self) -> MetricData[T]:
|
||||
"""Fetch from skipped position to end."""
|
||||
return self._config.get_json()
|
||||
|
||||
def csv(self) -> str:
|
||||
"""Execute the query and return CSV data as a string."""
|
||||
return self._config.get_csv()
|
||||
|
||||
|
||||
class ToBuilder(Generic[T]):
|
||||
"""Builder after calling to(end). Can chain with take_last() or from()."""
|
||||
|
||||
def __init__(self, config: _EndpointConfig):
|
||||
self._config = config
|
||||
|
||||
def take_last(self, n: int) -> RangeBuilder[T]:
|
||||
"""Take last n items before the end position."""
|
||||
end = self._config.end or 0
|
||||
return RangeBuilder(_EndpointConfig(
|
||||
self._config.client, self._config.name, self._config.index,
|
||||
end - n, end
|
||||
))
|
||||
|
||||
def from_(self, start: int) -> RangeBuilder[T]:
|
||||
"""Set the start position."""
|
||||
return RangeBuilder(_EndpointConfig(
|
||||
self._config.client, self._config.name, self._config.index,
|
||||
start, self._config.end
|
||||
))
|
||||
|
||||
def json(self) -> MetricData[T]:
|
||||
"""Execute the query and return parsed JSON data (from start of data to end)."""
|
||||
return self._config.get_json()
|
||||
|
||||
def csv(self) -> str:
|
||||
"""Execute the query and return CSV data as a string."""
|
||||
def fetch_csv(self) -> str:
|
||||
"""Fetch as CSV."""
|
||||
return self._config.get_csv()
|
||||
|
||||
|
||||
class MetricEndpointBuilder(Generic[T]):
|
||||
"""Initial builder for metric endpoint queries.
|
||||
"""Builder for metric endpoint queries.
|
||||
|
||||
Use method chaining to specify the data range, then call json() or csv() to execute.
|
||||
Use method chaining to specify the data range, then call fetch() or fetch_csv() to execute.
|
||||
|
||||
Examples:
|
||||
# Get all data
|
||||
endpoint.json()
|
||||
# Fetch all data
|
||||
data = endpoint.fetch()
|
||||
|
||||
# Get last 10 points
|
||||
endpoint.last(10).json()
|
||||
# Single item access
|
||||
data = endpoint[5].fetch()
|
||||
|
||||
# Get range [100, 200)
|
||||
endpoint.range(100, 200).json()
|
||||
# Slice syntax (Python-native)
|
||||
data = endpoint[:10].fetch() # First 10
|
||||
data = endpoint[-5:].fetch() # Last 5
|
||||
data = endpoint[100:110].fetch() # Range
|
||||
|
||||
# Get 10 points starting from position 100
|
||||
endpoint.from_(100).take(10).json()
|
||||
# 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.skip(100).take(10).fetch()
|
||||
"""
|
||||
|
||||
def __init__(self, client: BrkClientBase, name: str, index: Index):
|
||||
self._config = _EndpointConfig(client, name, index)
|
||||
|
||||
def first(self, n: int) -> RangeBuilder[T]:
|
||||
"""Fetch the first n data points."""
|
||||
@overload
|
||||
def __getitem__(self, key: int) -> SingleItemBuilder[T]: ...
|
||||
@overload
|
||||
def __getitem__(self, key: slice) -> RangeBuilder[T]: ...
|
||||
|
||||
def __getitem__(self, key: Union[int, slice]) -> Union[SingleItemBuilder[T], RangeBuilder[T]]:
|
||||
"""Access single item or slice.
|
||||
|
||||
Examples:
|
||||
endpoint[5] # Single item at index 5
|
||||
endpoint[:10] # First 10
|
||||
endpoint[-5:] # Last 5
|
||||
endpoint[100:110] # Range 100-109
|
||||
"""
|
||||
if isinstance(key, int):
|
||||
return SingleItemBuilder(_EndpointConfig(
|
||||
self._config.client, self._config.name, self._config.index,
|
||||
key, key + 1
|
||||
))
|
||||
return RangeBuilder(_EndpointConfig(
|
||||
self._config.client, self._config.name, self._config.index,
|
||||
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
|
||||
))
|
||||
|
||||
def last(self, n: int) -> RangeBuilder[T]:
|
||||
"""Fetch the last n data points."""
|
||||
def tail(self, n: int = 10) -> RangeBuilder[T]:
|
||||
"""Get the last n items (pandas-style)."""
|
||||
return RangeBuilder(_EndpointConfig(
|
||||
self._config.client, self._config.name, self._config.index,
|
||||
-n, None
|
||||
))
|
||||
|
||||
def range(self, start: int, end: int) -> RangeBuilder[T]:
|
||||
"""Set an explicit range [start, end)."""
|
||||
return RangeBuilder(_EndpointConfig(
|
||||
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,
|
||||
start, end
|
||||
n, None
|
||||
))
|
||||
|
||||
def from_(self, start: int) -> FromBuilder[T]:
|
||||
"""Set the start position. Chain with take() or to()."""
|
||||
return FromBuilder(_EndpointConfig(
|
||||
self._config.client, self._config.name, self._config.index,
|
||||
start, None
|
||||
))
|
||||
|
||||
def to(self, end: int) -> ToBuilder[T]:
|
||||
"""Set the end position. Chain with take_last() or from_()."""
|
||||
return ToBuilder(_EndpointConfig(
|
||||
self._config.client, self._config.name, self._config.index,
|
||||
None, end
|
||||
))
|
||||
|
||||
def json(self) -> MetricData[T]:
|
||||
"""Execute the query and return parsed JSON data (all data)."""
|
||||
def fetch(self) -> MetricData[T]:
|
||||
"""Fetch all data as parsed JSON."""
|
||||
return self._config.get_json()
|
||||
|
||||
def csv(self) -> str:
|
||||
"""Execute the query and return CSV data as a string (all data)."""
|
||||
def fetch_csv(self) -> str:
|
||||
"""Fetch all data as CSV string."""
|
||||
return self._config.get_csv()
|
||||
|
||||
def path(self) -> str:
|
||||
|
||||
@@ -26,7 +26,7 @@ pub fn generate_python_client(
|
||||
writeln!(output, "# Do not edit manually\n").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
"from typing import TypeVar, Generic, Any, Optional, List, Literal, TypedDict, Union, Protocol"
|
||||
"from typing import TypeVar, Generic, Any, Optional, List, Literal, TypedDict, Union, Protocol, overload"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, "from http.client import HTTPSConnection, HTTPConnection").unwrap();
|
||||
|
||||
@@ -6,8 +6,8 @@ use std::fmt::Write;
|
||||
use brk_types::TreeNode;
|
||||
|
||||
use crate::{
|
||||
ClientMetadata, GenericSyntax, PatternField, PythonSyntax, child_type_name, generate_leaf_field,
|
||||
get_node_fields, get_pattern_instance_base, prepare_tree_node, to_snake_case,
|
||||
ClientMetadata, GenericSyntax, PatternField, PythonSyntax, generate_leaf_field,
|
||||
prepare_tree_node, to_snake_case,
|
||||
};
|
||||
|
||||
/// Generate tree classes
|
||||
@@ -41,22 +41,16 @@ fn generate_tree_class(
|
||||
|
||||
// Generate child classes FIRST (post-order traversal)
|
||||
// This ensures children are defined before parent references them
|
||||
for (child_name, child_node) in ctx.children.iter() {
|
||||
if let TreeNode::Branch(grandchildren) = child_node {
|
||||
let child_fields = get_node_fields(grandchildren, pattern_lookup);
|
||||
|
||||
// Generate inline class if no pattern match OR pattern is not parameterizable
|
||||
if !metadata.is_parameterizable_fields(&child_fields) {
|
||||
let child_class = child_type_name(name, child_name);
|
||||
generate_tree_class(
|
||||
output,
|
||||
&child_class,
|
||||
child_node,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
generated,
|
||||
);
|
||||
}
|
||||
for child in &ctx.children {
|
||||
if child.should_inline {
|
||||
generate_tree_class(
|
||||
output,
|
||||
&child.inline_type_name,
|
||||
child.node,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
generated,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,45 +65,36 @@ fn generate_tree_class(
|
||||
.unwrap();
|
||||
|
||||
let syntax = PythonSyntax;
|
||||
for ((field, child_fields_opt), (child_name, child_node)) in
|
||||
ctx.fields_with_child_info.iter().zip(ctx.children.iter())
|
||||
{
|
||||
let py_type = metadata.resolve_tree_field_type(
|
||||
field,
|
||||
child_fields_opt.as_deref(),
|
||||
name,
|
||||
child_name,
|
||||
GenericSyntax::PYTHON,
|
||||
);
|
||||
let field_name_py = to_snake_case(&field.name);
|
||||
for child in &ctx.children {
|
||||
let field_name_py = to_snake_case(child.name);
|
||||
|
||||
if metadata.is_pattern_type(&field.rust_type) && metadata.is_parameterizable(&field.rust_type)
|
||||
{
|
||||
// Parameterizable pattern: use pattern class with metric base
|
||||
let metric_base = get_pattern_instance_base(child_node);
|
||||
writeln!(
|
||||
output,
|
||||
" self.{}: {} = {}(client, '{}')",
|
||||
field_name_py, py_type, field.rust_type, metric_base
|
||||
)
|
||||
.unwrap();
|
||||
} else if let TreeNode::Leaf(leaf) = child_node {
|
||||
// Leaf node: use shared helper
|
||||
generate_leaf_field(output, &syntax, "client", child_name, leaf, metadata, " ");
|
||||
} else if field.is_branch() {
|
||||
// Non-parameterizable pattern or regular branch: generate inline class
|
||||
let inline_class = child_type_name(name, &field.name);
|
||||
if child.is_leaf {
|
||||
if let TreeNode::Leaf(leaf) = child.node {
|
||||
generate_leaf_field(output, &syntax, "client", child.name, leaf, metadata, " ");
|
||||
}
|
||||
} else if child.should_inline {
|
||||
// Inline class
|
||||
writeln!(
|
||||
output,
|
||||
" self.{}: {} = {}(client)",
|
||||
field_name_py, inline_class, inline_class
|
||||
field_name_py, child.inline_type_name, child.inline_type_name
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
panic!(
|
||||
"Field '{}' has no matching index pattern. All metrics must be indexed.",
|
||||
field.name
|
||||
// Use pattern class with metric base
|
||||
let py_type = metadata.resolve_tree_field_type(
|
||||
&child.field,
|
||||
child.child_fields.as_deref(),
|
||||
name,
|
||||
child.name,
|
||||
GenericSyntax::PYTHON,
|
||||
);
|
||||
writeln!(
|
||||
output,
|
||||
" self.{}: {} = {}(client, '{}')",
|
||||
field_name_py, py_type, child.field.rust_type, child.base_result.base
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ pub fn generate_imports(output: &mut String) {
|
||||
writeln!(
|
||||
output,
|
||||
r#"use std::sync::Arc;
|
||||
use std::ops::{{Bound, RangeBounds}};
|
||||
use serde::de::DeserializeOwned;
|
||||
pub use brk_cohort::*;
|
||||
pub use brk_types::*;
|
||||
@@ -193,21 +194,30 @@ impl EndpointConfig {{
|
||||
|
||||
/// Initial builder for metric endpoint queries.
|
||||
///
|
||||
/// Use method chaining to specify the data range, then call `json()` or `csv()` to execute.
|
||||
/// Use method chaining to specify the data range, then call `fetch()` or `fetch_csv()` to execute.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// // Get all data
|
||||
/// endpoint.json()?;
|
||||
/// // Fetch all data
|
||||
/// let data = endpoint.fetch()?;
|
||||
///
|
||||
/// // Get last 10 points
|
||||
/// endpoint.last(10).json()?;
|
||||
/// // 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)
|
||||
/// endpoint.range(100, 200).json()?;
|
||||
/// let data = endpoint.range(100..200).fetch()?;
|
||||
///
|
||||
/// // Get 10 points starting from position 100
|
||||
/// endpoint.from(100).take(10).json()?;
|
||||
/// // 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()?;
|
||||
/// ```
|
||||
pub struct MetricEndpointBuilder<T> {{
|
||||
config: EndpointConfig,
|
||||
@@ -219,44 +229,59 @@ impl<T: DeserializeOwned> MetricEndpointBuilder<T> {{
|
||||
Self {{ config: EndpointConfig::new(client, name, index), _marker: std::marker::PhantomData }}
|
||||
}}
|
||||
|
||||
/// Fetch the first n data points.
|
||||
pub fn first(mut self, n: u64) -> RangeBuilder<T> {{
|
||||
self.config.end = Some(n as i64);
|
||||
/// Select a specific index position.
|
||||
pub fn get(mut self, index: usize) -> SingleItemBuilder<T> {{
|
||||
self.config.start = Some(index as i64);
|
||||
self.config.end = Some(index as i64 + 1);
|
||||
SingleItemBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||
}}
|
||||
|
||||
/// Select a range using Rust range syntax.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// endpoint.range(..10) // first 10
|
||||
/// 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> {{
|
||||
self.config.start = match range.start_bound() {{
|
||||
Bound::Included(&n) => Some(n as i64),
|
||||
Bound::Excluded(&n) => Some(n as i64 + 1),
|
||||
Bound::Unbounded => None,
|
||||
}};
|
||||
self.config.end = match range.end_bound() {{
|
||||
Bound::Included(&n) => Some(n as i64 + 1),
|
||||
Bound::Excluded(&n) => Some(n as i64),
|
||||
Bound::Unbounded => None,
|
||||
}};
|
||||
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||
}}
|
||||
|
||||
/// Fetch the last n data points.
|
||||
pub fn last(mut self, n: u64) -> RangeBuilder<T> {{
|
||||
/// Take the first n items.
|
||||
pub fn take(self, n: usize) -> RangeBuilder<T> {{
|
||||
self.range(..n)
|
||||
}}
|
||||
|
||||
/// Take the last n items.
|
||||
pub fn last(mut self, n: usize) -> RangeBuilder<T> {{
|
||||
self.config.start = Some(-(n as i64));
|
||||
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||
}}
|
||||
|
||||
/// Set an explicit range [start, end).
|
||||
pub fn range(mut self, start: i64, end: i64) -> RangeBuilder<T> {{
|
||||
self.config.start = Some(start);
|
||||
self.config.end = Some(end);
|
||||
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||
/// Skip the first n items. Chain with `take(n)` to get a range.
|
||||
pub fn skip(mut self, n: usize) -> SkippedBuilder<T> {{
|
||||
self.config.start = Some(n as i64);
|
||||
SkippedBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||
}}
|
||||
|
||||
/// Set the start position. Chain with `take(n)` or `to(end)`.
|
||||
pub fn from(mut self, start: i64) -> FromBuilder<T> {{
|
||||
self.config.start = Some(start);
|
||||
FromBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||
}}
|
||||
|
||||
/// Set the end position. Chain with `takeLast(n)` or `from(start)`.
|
||||
pub fn to(mut self, end: i64) -> ToBuilder<T> {{
|
||||
self.config.end = Some(end);
|
||||
ToBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||
}}
|
||||
|
||||
/// Execute the query and return parsed JSON data (all data).
|
||||
pub fn json(self) -> Result<MetricData<T>> {{
|
||||
/// Fetch all data as parsed JSON.
|
||||
pub fn fetch(self) -> Result<MetricData<T>> {{
|
||||
self.config.get_json(None)
|
||||
}}
|
||||
|
||||
/// Execute the query and return CSV data as a string (all data).
|
||||
pub fn csv(self) -> Result<String> {{
|
||||
/// Fetch all data as CSV string.
|
||||
pub fn fetch_csv(self) -> Result<String> {{
|
||||
self.config.get_text(Some("csv"))
|
||||
}}
|
||||
|
||||
@@ -266,82 +291,63 @@ impl<T: DeserializeOwned> MetricEndpointBuilder<T> {{
|
||||
}}
|
||||
}}
|
||||
|
||||
/// Builder after calling `from(start)`. Can chain with `take(n)` or `to(end)`.
|
||||
pub struct FromBuilder<T> {{
|
||||
/// Builder for single item access.
|
||||
pub struct SingleItemBuilder<T> {{
|
||||
config: EndpointConfig,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
}}
|
||||
|
||||
impl<T: DeserializeOwned> FromBuilder<T> {{
|
||||
/// Take n items from the start position.
|
||||
pub fn take(mut self, n: u64) -> RangeBuilder<T> {{
|
||||
impl<T: DeserializeOwned> SingleItemBuilder<T> {{
|
||||
/// Fetch the single item.
|
||||
pub fn fetch(self) -> Result<MetricData<T>> {{
|
||||
self.config.get_json(None)
|
||||
}}
|
||||
|
||||
/// Fetch the single item as CSV.
|
||||
pub fn fetch_csv(self) -> Result<String> {{
|
||||
self.config.get_text(Some("csv"))
|
||||
}}
|
||||
}}
|
||||
|
||||
/// Builder after calling `skip(n)`. Chain with `take(n)` to specify count.
|
||||
pub struct SkippedBuilder<T> {{
|
||||
config: EndpointConfig,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
}}
|
||||
|
||||
impl<T: DeserializeOwned> SkippedBuilder<T> {{
|
||||
/// Take n items after the skipped position.
|
||||
pub fn take(mut self, n: usize) -> RangeBuilder<T> {{
|
||||
let start = self.config.start.unwrap_or(0);
|
||||
self.config.end = Some(start + n as i64);
|
||||
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||
}}
|
||||
|
||||
/// Set the end position.
|
||||
pub fn to(mut self, end: i64) -> RangeBuilder<T> {{
|
||||
self.config.end = Some(end);
|
||||
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||
}}
|
||||
|
||||
/// Execute the query and return parsed JSON data (from start to end of data).
|
||||
pub fn json(self) -> Result<MetricData<T>> {{
|
||||
/// Fetch from the skipped position to the end.
|
||||
pub fn fetch(self) -> Result<MetricData<T>> {{
|
||||
self.config.get_json(None)
|
||||
}}
|
||||
|
||||
/// Execute the query and return CSV data as a string.
|
||||
pub fn csv(self) -> Result<String> {{
|
||||
/// Fetch from the skipped position to the end as CSV.
|
||||
pub fn fetch_csv(self) -> Result<String> {{
|
||||
self.config.get_text(Some("csv"))
|
||||
}}
|
||||
}}
|
||||
|
||||
/// Builder after calling `to(end)`. Can chain with `takeLast(n)` or `from(start)`.
|
||||
pub struct ToBuilder<T> {{
|
||||
config: EndpointConfig,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
}}
|
||||
|
||||
impl<T: DeserializeOwned> ToBuilder<T> {{
|
||||
/// Take last n items before the end position.
|
||||
pub fn take_last(mut self, n: u64) -> RangeBuilder<T> {{
|
||||
let end = self.config.end.unwrap_or(0);
|
||||
self.config.start = Some(end - n as i64);
|
||||
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||
}}
|
||||
|
||||
/// Set the start position.
|
||||
pub fn from(mut self, start: i64) -> RangeBuilder<T> {{
|
||||
self.config.start = Some(start);
|
||||
RangeBuilder {{ config: self.config, _marker: std::marker::PhantomData }}
|
||||
}}
|
||||
|
||||
/// Execute the query and return parsed JSON data (from start of data to end).
|
||||
pub fn json(self) -> Result<MetricData<T>> {{
|
||||
self.config.get_json(None)
|
||||
}}
|
||||
|
||||
/// Execute the query and return CSV data as a string.
|
||||
pub fn csv(self) -> Result<String> {{
|
||||
self.config.get_text(Some("csv"))
|
||||
}}
|
||||
}}
|
||||
|
||||
/// Final builder with range fully specified. Can only call `json()` or `csv()`.
|
||||
/// Builder with range fully specified.
|
||||
pub struct RangeBuilder<T> {{
|
||||
config: EndpointConfig,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
}}
|
||||
|
||||
impl<T: DeserializeOwned> RangeBuilder<T> {{
|
||||
/// Execute the query and return parsed JSON data.
|
||||
pub fn json(self) -> Result<MetricData<T>> {{
|
||||
/// Fetch the range as parsed JSON.
|
||||
pub fn fetch(self) -> Result<MetricData<T>> {{
|
||||
self.config.get_json(None)
|
||||
}}
|
||||
|
||||
/// Execute the query and return CSV data as a string.
|
||||
pub fn csv(self) -> Result<String> {{
|
||||
/// Fetch the range as CSV string.
|
||||
pub fn fetch_csv(self) -> Result<String> {{
|
||||
self.config.get_text(Some("csv"))
|
||||
}}
|
||||
}}
|
||||
|
||||
@@ -6,9 +6,8 @@ use std::fmt::Write;
|
||||
use brk_types::TreeNode;
|
||||
|
||||
use crate::{
|
||||
ClientMetadata, GenericSyntax, LanguageSyntax, PatternField, RustSyntax, child_type_name,
|
||||
generate_leaf_field, generate_tree_node_field, get_node_fields, get_pattern_instance_base,
|
||||
prepare_tree_node, to_snake_case,
|
||||
ClientMetadata, GenericSyntax, LanguageSyntax, PatternField, RustSyntax,
|
||||
generate_leaf_field, generate_tree_node_field, prepare_tree_node, to_snake_case,
|
||||
};
|
||||
|
||||
/// Generate tree structs.
|
||||
@@ -39,25 +38,29 @@ fn generate_tree_node(
|
||||
return;
|
||||
};
|
||||
|
||||
// Generate struct definition
|
||||
writeln!(output, "/// Metrics tree node.").unwrap();
|
||||
writeln!(output, "pub struct {} {{", name).unwrap();
|
||||
|
||||
for ((field, child_fields), (child_name, _)) in
|
||||
ctx.fields_with_child_info.iter().zip(ctx.children.iter())
|
||||
{
|
||||
let field_name = to_snake_case(&field.name);
|
||||
let type_annotation = metadata.resolve_tree_field_type(
|
||||
field,
|
||||
child_fields.as_deref(),
|
||||
name,
|
||||
child_name,
|
||||
GenericSyntax::RUST,
|
||||
);
|
||||
for child in &ctx.children {
|
||||
let field_name = to_snake_case(child.name);
|
||||
let type_annotation = if child.should_inline {
|
||||
child.inline_type_name.clone()
|
||||
} else {
|
||||
metadata.resolve_tree_field_type(
|
||||
&child.field,
|
||||
child.child_fields.as_deref(),
|
||||
name,
|
||||
child.name,
|
||||
GenericSyntax::RUST,
|
||||
)
|
||||
};
|
||||
writeln!(output, " pub {}: {},", field_name, type_annotation).unwrap();
|
||||
}
|
||||
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
|
||||
// Generate impl block
|
||||
writeln!(output, "impl {} {{", name).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
@@ -67,53 +70,40 @@ fn generate_tree_node(
|
||||
writeln!(output, " Self {{").unwrap();
|
||||
|
||||
let syntax = RustSyntax;
|
||||
for ((field_info, child_fields), (child_name, child_node)) in
|
||||
ctx.fields_with_child_info.iter().zip(ctx.children.iter())
|
||||
{
|
||||
let field_name = to_snake_case(&field_info.name);
|
||||
for child in &ctx.children {
|
||||
let field_name = to_snake_case(child.name);
|
||||
|
||||
// Check if this is a pattern type and if it's parameterizable
|
||||
let is_parameterizable = child_fields
|
||||
.as_ref()
|
||||
.is_some_and(|cf| metadata.is_parameterizable_fields(cf));
|
||||
|
||||
if metadata.is_pattern_type(&field_info.rust_type) && is_parameterizable {
|
||||
// Parameterizable pattern: use pattern constructor with metric base
|
||||
let pattern_base = get_pattern_instance_base(child_node);
|
||||
generate_tree_node_field(
|
||||
output,
|
||||
&syntax,
|
||||
field_info,
|
||||
metadata,
|
||||
" ",
|
||||
child_name,
|
||||
Some(&pattern_base),
|
||||
);
|
||||
} else if child_fields.is_some() {
|
||||
// Non-parameterizable pattern or regular branch: use inline struct
|
||||
let child_struct = child_type_name(name, child_name);
|
||||
let path_expr = syntax.path_expr("base_path", &format!("_{}", child_name));
|
||||
if child.is_leaf {
|
||||
if let TreeNode::Leaf(leaf) = child.node {
|
||||
generate_leaf_field(
|
||||
output,
|
||||
&syntax,
|
||||
"client.clone()",
|
||||
child.name,
|
||||
leaf,
|
||||
metadata,
|
||||
" ",
|
||||
);
|
||||
}
|
||||
} else if child.should_inline {
|
||||
// Inline struct
|
||||
let path_expr = syntax.path_expr("base_path", &format!("_{}", child.name));
|
||||
writeln!(
|
||||
output,
|
||||
" {}: {}::new(client.clone(), {}),",
|
||||
field_name, child_struct, path_expr
|
||||
field_name, child.inline_type_name, path_expr
|
||||
)
|
||||
.unwrap();
|
||||
} else if let TreeNode::Leaf(leaf) = child_node {
|
||||
// Leaf field - use shared helper
|
||||
generate_leaf_field(
|
||||
} else {
|
||||
// Use pattern constructor
|
||||
generate_tree_node_field(
|
||||
output,
|
||||
&syntax,
|
||||
"client.clone()",
|
||||
child_name,
|
||||
leaf,
|
||||
&child.field,
|
||||
metadata,
|
||||
" ",
|
||||
);
|
||||
} else {
|
||||
panic!(
|
||||
"Field '{}' is a leaf with no TreeNode::Leaf. This shouldn't happen.",
|
||||
field_info.name
|
||||
child.name,
|
||||
Some(&child.base_result.base),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -122,21 +112,17 @@ fn generate_tree_node(
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
|
||||
for (child_name, child_node) in ctx.children {
|
||||
if let TreeNode::Branch(grandchildren) = child_node {
|
||||
let child_fields = get_node_fields(grandchildren, pattern_lookup);
|
||||
// Generate child struct if no pattern match OR pattern is not parameterizable
|
||||
if !metadata.is_parameterizable_fields(&child_fields) {
|
||||
let child_struct = child_type_name(name, child_name);
|
||||
generate_tree_node(
|
||||
output,
|
||||
&child_struct,
|
||||
child_node,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
generated,
|
||||
);
|
||||
}
|
||||
// Generate child structs
|
||||
for child in &ctx.children {
|
||||
if child.should_inline {
|
||||
generate_tree_node(
|
||||
output,
|
||||
&child.inline_type_name,
|
||||
child.node,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
generated,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ brk_rpc = { workspace = true }
|
||||
brk_server = { workspace = true }
|
||||
clap = { version = "4.5.54", features = ["derive", "string"] }
|
||||
color-eyre = { workspace = true }
|
||||
importmap = "0.1.2"
|
||||
importmap = "0.1.3"
|
||||
# importmap = { path = "../../../importmap" }
|
||||
tracing = { workspace = true }
|
||||
minreq = { workspace = true }
|
||||
|
||||
+40
-27
@@ -17,6 +17,7 @@ use brk_mempool::Mempool;
|
||||
use brk_query::AsyncQuery;
|
||||
use brk_reader::Reader;
|
||||
use brk_server::{Server, VERSION};
|
||||
use importmap::ImportMap;
|
||||
use tracing::info;
|
||||
use vecdb::Exit;
|
||||
|
||||
@@ -85,25 +86,28 @@ pub fn run() -> color_eyre::Result<()> {
|
||||
let data_path = config.brkdir();
|
||||
|
||||
let future = async move {
|
||||
let bundle_path = if website.is_some() {
|
||||
// Try to find local dev directories - check cwd and parent directories
|
||||
let find_dev_dirs = || -> Option<(PathBuf, PathBuf)> {
|
||||
let mut dir = std::env::current_dir().ok()?;
|
||||
loop {
|
||||
let websites = dir.join("websites");
|
||||
let modules = dir.join("modules");
|
||||
if websites.exists() && modules.exists() {
|
||||
return Some((websites, modules));
|
||||
}
|
||||
// Stop at workspace root (crates/ indicates we're there)
|
||||
if dir.join("crates").exists() {
|
||||
return None;
|
||||
}
|
||||
dir = dir.parent()?.to_path_buf();
|
||||
// Try to find local dev directories - check cwd and parent directories
|
||||
let find_dev_dirs = || -> Option<(PathBuf, PathBuf)> {
|
||||
let mut dir = std::env::current_dir().ok()?;
|
||||
loop {
|
||||
let websites = dir.join("websites");
|
||||
let modules = dir.join("modules");
|
||||
if websites.exists() && modules.exists() {
|
||||
return Some((websites, modules));
|
||||
}
|
||||
};
|
||||
// Stop at workspace root (crates/ indicates we're there)
|
||||
if dir.join("crates").exists() {
|
||||
return None;
|
||||
}
|
||||
dir = dir.parent()?.to_path_buf();
|
||||
}
|
||||
};
|
||||
|
||||
let websites_path = if let Some((websites, _modules)) = find_dev_dirs() {
|
||||
let dev_dirs = find_dev_dirs();
|
||||
let is_dev = dev_dirs.is_some();
|
||||
|
||||
let bundle_path = if website.is_some() {
|
||||
let websites_path = if let Some((websites, _modules)) = dev_dirs {
|
||||
websites
|
||||
} else {
|
||||
let downloaded_brk_path = downloads_path.join(format!("brk-{VERSION}"));
|
||||
@@ -133,19 +137,28 @@ pub fn run() -> color_eyre::Result<()> {
|
||||
None
|
||||
};
|
||||
|
||||
// Generate import map for cache busting
|
||||
// Generate import map for cache busting (disabled in dev mode)
|
||||
if let Some(ref path) = bundle_path {
|
||||
match importmap::ImportMap::scan(path, "") {
|
||||
Ok(map) => {
|
||||
let html_path = path.join("index.html");
|
||||
if let Ok(html) = fs::read_to_string(&html_path)
|
||||
&& let Some(updated) = map.update_html(&html)
|
||||
{
|
||||
let _ = fs::write(&html_path, updated);
|
||||
info!("Updated importmap in index.html");
|
||||
let map = if is_dev {
|
||||
ImportMap::empty()
|
||||
} else {
|
||||
match ImportMap::scan(path, "") {
|
||||
Ok(map) => map,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to generate importmap: {e}");
|
||||
ImportMap::empty()
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::error!("Failed to generate importmap: {e}"),
|
||||
};
|
||||
|
||||
let html_path = path.join("index.html");
|
||||
if let Ok(html) = fs::read_to_string(&html_path)
|
||||
&& let Some(updated) = map.update_html(&html)
|
||||
{
|
||||
let _ = fs::write(&html_path, updated);
|
||||
if !is_dev {
|
||||
info!("Updated importmap in index.html");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ fn main() -> brk_client::Result<()> {
|
||||
});
|
||||
|
||||
// Fetch price data using the typed metrics API
|
||||
// Using new idiomatic API: last(3).fetch()
|
||||
let price_close = client
|
||||
.metrics()
|
||||
.price
|
||||
@@ -22,8 +23,8 @@ fn main() -> brk_client::Result<()> {
|
||||
.close
|
||||
.by
|
||||
.dateindex()
|
||||
.from(-3)
|
||||
.json()?;
|
||||
.last(3)
|
||||
.fetch()?;
|
||||
println!("Last 3 price close values: {:?}", price_close);
|
||||
|
||||
// Fetch block data
|
||||
@@ -35,12 +36,11 @@ fn main() -> brk_client::Result<()> {
|
||||
.sum
|
||||
.by
|
||||
.dateindex()
|
||||
.from(-3)
|
||||
.json()?;
|
||||
.last(3)
|
||||
.fetch()?;
|
||||
println!("Last 3 block count values: {:?}", block_count);
|
||||
|
||||
// Fetch supply data
|
||||
//
|
||||
dbg!(
|
||||
client
|
||||
.metrics()
|
||||
@@ -58,8 +58,8 @@ fn main() -> brk_client::Result<()> {
|
||||
.bitcoin
|
||||
.by
|
||||
.dateindex()
|
||||
.from(-3)
|
||||
.csv()?;
|
||||
.last(3)
|
||||
.fetch_csv()?;
|
||||
println!("Last 3 circulating supply values: {:?}", circulating);
|
||||
|
||||
// Using generic metric fetching
|
||||
|
||||
+1007
-2246
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
||||
use std::{env, path::Path, thread};
|
||||
use std::{env, path::Path, thread, time::Instant};
|
||||
|
||||
use brk_computer::Computer;
|
||||
use brk_error::Result;
|
||||
use brk_fetcher::Fetcher;
|
||||
use brk_indexer::Indexer;
|
||||
use vecdb::{AnyStoredVec, Exit};
|
||||
use vecdb::{AnySerializableVec, AnyVec, Exit};
|
||||
|
||||
pub fn main() -> Result<()> {
|
||||
// Can't increase main thread's stack size, thus we need to use another thread
|
||||
@@ -19,7 +19,6 @@ fn run() -> Result<()> {
|
||||
brk_logger::init(None)?;
|
||||
|
||||
let outputs_dir = Path::new(&env::var("HOME").unwrap()).join(".brk");
|
||||
// let outputs_dir = Path::new("../../_outputs");
|
||||
|
||||
let indexer = Indexer::forced_import(&outputs_dir)?;
|
||||
|
||||
@@ -30,7 +29,35 @@ fn run() -> Result<()> {
|
||||
|
||||
let computer = Computer::forced_import(&outputs_dir, &indexer, Some(fetcher))?;
|
||||
|
||||
let _a = dbg!(computer.transactions.fees.fee.base.region().meta());
|
||||
// Test emptyaddressdata (underlying BytesVec) - direct access
|
||||
let empty_data = &computer.distribution.addresses_data.empty;
|
||||
println!("emptyaddressdata (BytesVec) len: {}", empty_data.len());
|
||||
|
||||
let start = Instant::now();
|
||||
let mut buf = Vec::new();
|
||||
empty_data.write_json(Some(empty_data.len() - 1), Some(empty_data.len()), &mut buf)?;
|
||||
println!("emptyaddressdata last item JSON: {}", String::from_utf8_lossy(&buf));
|
||||
println!("Time for BytesVec write_json: {:?}", start.elapsed());
|
||||
|
||||
// Test emptyaddressindex (LazyVecFrom1 wrapper) - computed access
|
||||
let empty_index = &computer.distribution.emptyaddressindex;
|
||||
println!("\nemptyaddressindex (LazyVecFrom1) len: {}", empty_index.len());
|
||||
|
||||
let start = Instant::now();
|
||||
let mut buf = Vec::new();
|
||||
empty_index.write_json(Some(empty_index.len() - 1), Some(empty_index.len()), &mut buf)?;
|
||||
println!("emptyaddressindex last item JSON: {}", String::from_utf8_lossy(&buf));
|
||||
println!("Time for LazyVecFrom1 write_json: {:?}", start.elapsed());
|
||||
|
||||
// Compare with loaded versions
|
||||
let loaded_data = &computer.distribution.addresses_data.loaded;
|
||||
println!("\nloadedaddressdata (BytesVec) len: {}", loaded_data.len());
|
||||
|
||||
let start = Instant::now();
|
||||
let mut buf = Vec::new();
|
||||
loaded_data.write_json(Some(loaded_data.len() - 1), Some(loaded_data.len()), &mut buf)?;
|
||||
println!("loadedaddressdata last item JSON: {}", String::from_utf8_lossy(&buf));
|
||||
println!("Time for BytesVec write_json: {:?}", start.elapsed());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -137,6 +137,18 @@ pub enum Error {
|
||||
|
||||
|
||||
impl Error {
|
||||
/// Returns true if this error is due to a file lock (another process has the database open).
|
||||
/// Lock errors are transient and should not trigger data deletion.
|
||||
pub fn is_lock_error(&self) -> bool {
|
||||
matches!(self, Error::VecDB(e) if e.is_lock_error())
|
||||
}
|
||||
|
||||
/// Returns true if this error indicates data corruption or version incompatibility.
|
||||
/// These errors may require resetting/deleting the data to recover.
|
||||
pub fn is_data_error(&self) -> bool {
|
||||
matches!(self, Error::VecDB(e) if e.is_data_error())
|
||||
}
|
||||
|
||||
/// Returns true if this network/fetch error indicates a permanent/blocking condition
|
||||
/// that won't be resolved by retrying (e.g., DNS failure, connection refused, blocked endpoint).
|
||||
/// Returns false for transient errors worth retrying (timeouts, rate limits, server errors).
|
||||
|
||||
@@ -47,27 +47,26 @@ impl Indexer {
|
||||
let indexed_path = outputs_dir.join("indexed");
|
||||
|
||||
let try_import = || -> Result<Self> {
|
||||
let (vecs, stores) = thread::scope(|s| -> Result<_> {
|
||||
let vecs = s.spawn(|| -> Result<_> {
|
||||
let i = Instant::now();
|
||||
let vecs = Vecs::forced_import(&indexed_path, VERSION)?;
|
||||
info!("Imported vecs in {:?}", i.elapsed());
|
||||
Ok(vecs)
|
||||
});
|
||||
let i = Instant::now();
|
||||
let vecs = Vecs::forced_import(&indexed_path, VERSION)?;
|
||||
info!("Imported vecs in {:?}", i.elapsed());
|
||||
|
||||
let i = Instant::now();
|
||||
let stores = Stores::forced_import(&indexed_path, VERSION)?;
|
||||
info!("Imported stores in {:?}", i.elapsed());
|
||||
|
||||
Ok((vecs.join().unwrap()?, stores))
|
||||
})?;
|
||||
let i = Instant::now();
|
||||
let stores = Stores::forced_import(&indexed_path, VERSION)?;
|
||||
info!("Imported stores in {:?}", i.elapsed());
|
||||
|
||||
Ok(Self { vecs, stores })
|
||||
};
|
||||
|
||||
match try_import() {
|
||||
Ok(result) => Ok(result),
|
||||
Err(err) if can_retry => {
|
||||
Err(err) if err.is_lock_error() => {
|
||||
// Lock errors are transient - another process has the database open.
|
||||
// Don't delete data, just return the error.
|
||||
Err(err)
|
||||
}
|
||||
Err(err) if can_retry && err.is_data_error() => {
|
||||
// Data corruption or version mismatch - safe to delete and retry
|
||||
info!("{err:?}, deleting {indexed_path:?} and retrying");
|
||||
fs::remove_dir_all(&indexed_path)?;
|
||||
Self::forced_import_inner(outputs_dir, false)
|
||||
|
||||
Reference in New Issue
Block a user