Files
brk/crates/brk_binder/src/python.rs
T
2025-12-21 00:33:56 +01:00

931 lines
32 KiB
Rust

use std::collections::HashSet;
use std::fmt::Write as FmtWrite;
use std::fs;
use std::io;
use std::path::Path;
use brk_types::{Index, TreeNode};
use serde_json::Value;
use crate::{
ClientMetadata, Endpoint, FieldNamePosition, IndexSetPattern, PatternField, StructuralPattern,
TypeSchemas, extract_inner_type, get_node_fields, get_pattern_instance_base, is_enum_schema,
to_pascal_case, to_snake_case, unwrap_allof,
};
/// Generate Python client from metadata and OpenAPI endpoints
pub fn generate_python_client(
metadata: &ClientMetadata,
endpoints: &[Endpoint],
schemas: &TypeSchemas,
output_dir: &Path,
) -> io::Result<()> {
let mut output = String::new();
// Header
writeln!(output, "# Auto-generated BRK Python client").unwrap();
writeln!(output, "# Do not edit manually\n").unwrap();
writeln!(output, "from __future__ import annotations").unwrap();
writeln!(
output,
"from typing import TypeVar, Generic, Any, Optional, List, Literal, TypedDict"
)
.unwrap();
writeln!(output, "import httpx\n").unwrap();
// Type variable for generic MetricNode
writeln!(output, "T = TypeVar('T')\n").unwrap();
// Generate type definitions from OpenAPI schemas (now includes leaf types from catalog)
generate_type_definitions(&mut output, schemas);
// Generate base client class
generate_base_client(&mut output);
// Generate MetricNode class
generate_metric_node(&mut output);
// Generate index accessor classes
generate_index_accessors(&mut output, &metadata.index_set_patterns);
// Generate structural pattern classes
generate_structural_patterns(&mut output, &metadata.structural_patterns, metadata);
// Generate tree classes
generate_tree_classes(&mut output, &metadata.catalog, metadata);
// Generate main client with tree and API methods
generate_main_client(&mut output, endpoints);
fs::write(output_dir.join("client.py"), output)?;
Ok(())
}
/// Generate Python type definitions from OpenAPI schemas
fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
if schemas.is_empty() {
return;
}
writeln!(output, "# Type definitions\n").unwrap();
// Sort types by dependencies (types that reference other types must come after)
let sorted_names = topological_sort_schemas(schemas);
for name in sorted_names {
let Some(schema) = schemas.get(&name) else {
continue;
};
if let Some(props) = schema.get("properties").and_then(|p| p.as_object()) {
// Object type -> TypedDict
writeln!(output, "class {}(TypedDict):", name).unwrap();
for (prop_name, prop_schema) in props {
let prop_type = schema_to_python_type(prop_schema);
let safe_name = escape_python_keyword(prop_name);
writeln!(output, " {}: {}", safe_name, prop_type).unwrap();
}
writeln!(output).unwrap();
} else if is_enum_schema(schema) {
// Enum type -> Literal union
let py_type = schema_to_python_type(schema);
writeln!(output, "{} = {}", name, py_type).unwrap();
} else {
// Primitive type alias
let py_type = schema_to_python_type(schema);
writeln!(output, "{} = {}", name, py_type).unwrap();
}
}
writeln!(output).unwrap();
}
/// Topologically sort schema names so dependencies come before dependents.
/// Types that reference other types (via $ref) must be defined after their dependencies.
fn topological_sort_schemas(schemas: &TypeSchemas) -> Vec<String> {
use std::collections::{HashMap, HashSet};
// Build dependency graph
let mut deps: HashMap<String, HashSet<String>> = HashMap::new();
for (name, schema) in schemas {
let mut type_deps = HashSet::new();
collect_schema_refs(schema, &mut type_deps);
// Only keep deps that are in our schemas
type_deps.retain(|d| schemas.contains_key(d));
deps.insert(name.clone(), type_deps);
}
// Kahn's algorithm for topological sort
let mut in_degree: HashMap<String, usize> = HashMap::new();
for name in schemas.keys() {
in_degree.insert(name.clone(), 0);
}
for type_deps in deps.values() {
for dep in type_deps {
*in_degree.entry(dep.clone()).or_insert(0) += 1;
}
}
// Start with types that have no dependents (are not referenced by others)
let mut queue: Vec<String> = in_degree
.iter()
.filter(|(_, count)| **count == 0)
.map(|(name, _)| name.clone())
.collect();
queue.sort(); // Deterministic order
let mut result = Vec::new();
while let Some(name) = queue.pop() {
result.push(name.clone());
if let Some(type_deps) = deps.get(&name) {
for dep in type_deps {
if let Some(count) = in_degree.get_mut(dep) {
*count = count.saturating_sub(1);
if *count == 0 {
queue.push(dep.clone());
queue.sort(); // Keep sorted for determinism
}
}
}
}
}
// Reverse so dependencies come first
result.reverse();
// Add any types that weren't processed (e.g., due to circular refs or other edge cases)
let result_set: HashSet<_> = result.iter().cloned().collect();
let mut missing: Vec<_> = schemas
.keys()
.filter(|k| !result_set.contains(*k))
.cloned()
.collect();
missing.sort();
result.extend(missing);
result
}
/// Collect all type references ($ref) from a schema
fn collect_schema_refs(schema: &Value, refs: &mut std::collections::HashSet<String>) {
match schema {
Value::Object(map) => {
if let Some(ref_path) = map.get("$ref").and_then(|r| r.as_str()) {
if let Some(type_name) = ref_path.rsplit('/').next() {
refs.insert(type_name.to_string());
}
}
for value in map.values() {
collect_schema_refs(value, refs);
}
}
Value::Array(arr) => {
for item in arr {
collect_schema_refs(item, refs);
}
}
_ => {}
}
}
/// Convert JSON Schema to Python type
fn schema_to_python_type(schema: &Value) -> String {
// Unwrap single-element allOf (schemars uses this for composition)
let schema = unwrap_allof(schema);
// Handle $ref
if let Some(ref_path) = schema.get("$ref").and_then(|r| r.as_str()) {
return ref_path.rsplit('/').next().unwrap_or("Any").to_string();
}
// Handle enum (array of string values)
if let Some(enum_values) = schema.get("enum").and_then(|e| e.as_array()) {
let literals: Vec<String> = enum_values
.iter()
.filter_map(|v| v.as_str())
.map(|s| format!("\"{}\"", s))
.collect();
if !literals.is_empty() {
return format!("Literal[{}]", literals.join(", "));
}
}
// Handle type field
if let Some(ty) = schema.get("type").and_then(|t| t.as_str()) {
return match ty {
"integer" => "int".to_string(),
"number" => "float".to_string(),
"boolean" => "bool".to_string(),
"string" => "str".to_string(),
"null" => "None".to_string(),
"array" => {
let item_type = schema
.get("items")
.map(schema_to_python_type)
.unwrap_or_else(|| "Any".to_string());
format!("List[{}]", item_type)
}
"object" => "dict".to_string(),
_ => "Any".to_string(),
};
}
// Handle anyOf/oneOf
if let Some(variants) = schema
.get("anyOf")
.or_else(|| schema.get("oneOf"))
.and_then(|v| v.as_array())
{
let types: Vec<String> = variants.iter().map(schema_to_python_type).collect();
return types.join(" | ");
}
"Any".to_string()
}
/// Make a name safe for Python: escape keywords and prefix digit-starting names
fn escape_python_keyword(name: &str) -> String {
const PYTHON_KEYWORDS: &[&str] = &[
"False", "None", "True", "and", "as", "assert", "async", "await", "break", "class",
"continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global",
"if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return",
"try", "while", "with", "yield",
];
// Names starting with digit need underscore prefix
let name = if name.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) {
format!("_{}", name)
} else {
name.to_string()
};
// Reserved keywords get underscore suffix
if PYTHON_KEYWORDS.contains(&name.as_str()) {
format!("{}_", name)
} else {
name
}
}
/// Generate the base BrkClient class with HTTP functionality
fn generate_base_client(output: &mut String) {
writeln!(
output,
r#"class BrkError(Exception):
"""Custom error class for BRK client errors."""
def __init__(self, message: str, status: Optional[int] = None):
super().__init__(message)
self.status = status
class BrkClientBase:
"""Base HTTP client for making requests."""
def __init__(self, base_url: str, timeout: float = 30.0):
self.base_url = base_url
self.timeout = timeout
self._client = httpx.Client(timeout=timeout)
def get(self, path: str) -> Any:
"""Make a GET request."""
try:
response = self._client.get(f"{{self.base_url}}{{path}}")
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as e:
raise BrkError(f"HTTP error: {{e.response.status_code}}", e.response.status_code)
except httpx.RequestError as e:
raise BrkError(str(e))
def close(self):
"""Close the HTTP client."""
self._client.close()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
"#
)
.unwrap();
}
/// Generate the MetricNode class
fn generate_metric_node(output: &mut String) {
writeln!(
output,
r#"class MetricNode(Generic[T]):
"""A metric node that can fetch data for different indexes."""
def __init__(self, client: BrkClientBase, path: str):
self._client = client
self._path = path
def get(self) -> List[T]:
"""Fetch all data points for this metric."""
return self._client.get(self._path)
def get_range(self, from_date: str, to_date: str) -> List[T]:
"""Fetch data points within a date range."""
return self._client.get(f"{{self._path}}?from={{from_date}}&to={{to_date}}")
"#
)
.unwrap();
}
/// Generate index accessor classes
fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern]) {
if patterns.is_empty() {
return;
}
writeln!(output, "# Index accessor classes\n").unwrap();
for pattern in patterns {
writeln!(output, "class {}(Generic[T]):", pattern.name).unwrap();
writeln!(
output,
" \"\"\"Index accessor for metrics with {} indexes.\"\"\"",
pattern.indexes.len()
)
.unwrap();
writeln!(output, " ").unwrap();
writeln!(
output,
" def __init__(self, client: BrkClientBase, base_path: str):"
)
.unwrap();
for index in &pattern.indexes {
let field_name = index_to_snake_case(index);
let path_segment = index.serialize_long();
writeln!(
output,
" self.{}: MetricNode[T] = MetricNode(client, f'{{base_path}}/{}')",
field_name, path_segment
)
.unwrap();
}
writeln!(output).unwrap();
}
}
/// Convert an Index to a snake_case field name (e.g., DateIndex -> by_date_index)
fn index_to_snake_case(index: &Index) -> String {
format!("by_{}", to_snake_case(index.serialize_long()))
}
/// Generate structural pattern classes
fn generate_structural_patterns(
output: &mut String,
patterns: &[StructuralPattern],
metadata: &ClientMetadata,
) {
if patterns.is_empty() {
return;
}
writeln!(output, "# Reusable structural pattern classes\n").unwrap();
for pattern in patterns {
let is_parameterizable = pattern.is_parameterizable();
// For generic patterns, inherit from Generic[T]
if pattern.is_generic {
writeln!(output, "class {}(Generic[T]):", pattern.name).unwrap();
} else {
writeln!(output, "class {}:", pattern.name).unwrap();
}
writeln!(
output,
" \"\"\"Pattern struct for repeated tree structure.\"\"\""
)
.unwrap();
writeln!(output, " ").unwrap();
if is_parameterizable {
writeln!(
output,
" def __init__(self, client: BrkClientBase, acc: str):"
)
.unwrap();
writeln!(
output,
" \"\"\"Create pattern node with accumulated metric name.\"\"\""
)
.unwrap();
} else {
writeln!(
output,
" def __init__(self, client: BrkClientBase, base_path: str):"
)
.unwrap();
}
for field in &pattern.fields {
if is_parameterizable {
generate_parameterized_python_field(output, field, pattern, metadata);
} else {
generate_tree_path_python_field(output, field, metadata);
}
}
writeln!(output).unwrap();
}
}
/// Generate a field using parameterized (prepend/append) metric name construction
fn generate_parameterized_python_field(
output: &mut String,
field: &PatternField,
pattern: &StructuralPattern,
metadata: &ClientMetadata,
) {
let field_name = to_snake_case(&field.name);
let py_type = field_to_python_type_generic(field, metadata, pattern.is_generic);
// For branch fields, pass the accumulated name to nested pattern
if metadata.is_pattern_type(&field.rust_type) {
let child_acc = if let Some(pos) = pattern.get_field_position(&field.name) {
match pos {
FieldNamePosition::Append(suffix) => format!("f'{{acc}}{}'", suffix),
FieldNamePosition::Prepend(prefix) => format!("f'{}{{acc}}'", prefix),
FieldNamePosition::Identity => "acc".to_string(),
FieldNamePosition::SetBase(base) => format!("'{}'", base),
}
} else {
format!("f'{{acc}}_{}'", field.name)
};
writeln!(
output,
" self.{}: {} = {}(client, {})",
field_name, py_type, field.rust_type, child_acc
)
.unwrap();
return;
}
// For leaf fields, construct the metric path based on position
let metric_expr = if let Some(pos) = pattern.get_field_position(&field.name) {
match pos {
FieldNamePosition::Append(suffix) => format!("f'/{{acc}}{}'", suffix),
FieldNamePosition::Prepend(prefix) => format!("f'/{}{{acc}}'", prefix),
FieldNamePosition::Identity => "f'/{acc}'".to_string(),
FieldNamePosition::SetBase(base) => format!("'/{}'", base),
}
} else {
format!("f'/{{acc}}_{}'", field.name)
};
if field_uses_accessor(field, metadata) {
let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap();
writeln!(
output,
" self.{}: {} = {}(client, {})",
field_name, py_type, accessor.name, metric_expr
)
.unwrap();
} else {
writeln!(
output,
" self.{}: {} = MetricNode(client, {})",
field_name, py_type, metric_expr
)
.unwrap();
}
}
/// Generate a field using tree path construction (fallback for non-parameterizable patterns)
fn generate_tree_path_python_field(
output: &mut String,
field: &PatternField,
metadata: &ClientMetadata,
) {
let field_name = to_snake_case(&field.name);
let py_type = field_to_python_type(field, metadata);
if metadata.is_pattern_type(&field.rust_type) {
writeln!(
output,
" self.{}: {} = {}(client, f'{{base_path}}/{}')",
field_name, py_type, field.rust_type, field.name
)
.unwrap();
} else if field_uses_accessor(field, metadata) {
let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap();
writeln!(
output,
" self.{}: {} = {}(client, f'{{base_path}}/{}')",
field_name, py_type, accessor.name, field.name
)
.unwrap();
} else {
writeln!(
output,
" self.{}: {} = MetricNode(client, f'{{base_path}}/{}')",
field_name, py_type, field.name
)
.unwrap();
}
}
/// Convert pattern field to Python type annotation
fn field_to_python_type(field: &PatternField, metadata: &ClientMetadata) -> String {
field_to_python_type_generic(field, metadata, false)
}
/// Convert pattern field to Python type annotation, with optional generic support
fn field_to_python_type_generic(
field: &PatternField,
metadata: &ClientMetadata,
is_generic: bool,
) -> String {
field_to_python_type_with_generic_value(field, metadata, is_generic, None)
}
/// Convert pattern field to Python type annotation.
/// - `is_generic`: If true and field.rust_type is "T", use T in the output
/// - `generic_value_type`: For branch fields that reference a generic pattern, this is the concrete type to substitute
fn field_to_python_type_with_generic_value(
field: &PatternField,
metadata: &ClientMetadata,
is_generic: bool,
generic_value_type: Option<&str>,
) -> String {
// For generic patterns, use T instead of concrete value type
// Also extract inner type from wrappers like Close<Dollars> -> Dollars
let value_type = if is_generic && field.rust_type == "T" {
"T".to_string()
} else {
extract_inner_type(&field.rust_type)
};
if metadata.is_pattern_type(&field.rust_type) {
// Check if this pattern is generic and we have a value type
if metadata.is_pattern_generic(&field.rust_type)
&& let Some(vt) = generic_value_type
{
return format!("{}[{}]", field.rust_type, vt);
}
field.rust_type.clone()
} else if let Some(accessor) = metadata.find_index_set_pattern(&field.indexes) {
// Leaf with accessor - use value_type as the generic
format!("{}[{}]", accessor.name, value_type)
} else {
// Leaf - use value_type as the generic
format!("MetricNode[{}]", value_type)
}
}
/// Check if a field should use an index accessor
fn field_uses_accessor(field: &PatternField, metadata: &ClientMetadata) -> bool {
metadata.find_index_set_pattern(&field.indexes).is_some()
}
/// Generate tree classes
fn generate_tree_classes(output: &mut String, catalog: &TreeNode, metadata: &ClientMetadata) {
writeln!(output, "# Catalog tree classes\n").unwrap();
let pattern_lookup = metadata.pattern_lookup();
let mut generated = HashSet::new();
generate_tree_class(
output,
"CatalogTree",
catalog,
&pattern_lookup,
metadata,
&mut generated,
);
}
/// Recursively generate tree classes
fn generate_tree_class(
output: &mut String,
name: &str,
node: &TreeNode,
pattern_lookup: &std::collections::HashMap<Vec<PatternField>, String>,
metadata: &ClientMetadata,
generated: &mut HashSet<String>,
) {
if let TreeNode::Branch(children) = node {
// Build signature with child field info for generic pattern lookup
let fields_with_child_info: Vec<(PatternField, Option<Vec<PatternField>>)> = children
.iter()
.map(|(child_name, child_node)| {
let (rust_type, json_type, indexes, child_fields) = match child_node {
TreeNode::Leaf(leaf) => (
leaf.value_type().to_string(),
leaf.schema
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("object")
.to_string(),
leaf.indexes().clone(),
None,
),
TreeNode::Branch(grandchildren) => {
let child_fields = get_node_fields(grandchildren, pattern_lookup);
let pattern_name = pattern_lookup
.get(&child_fields)
.cloned()
.unwrap_or_else(|| format!("{}_{}", name, to_pascal_case(child_name)));
(
pattern_name.clone(),
pattern_name,
std::collections::BTreeSet::new(),
Some(child_fields),
)
}
};
(
PatternField {
name: child_name.clone(),
rust_type,
json_type,
indexes,
},
child_fields,
)
})
.collect();
let fields: Vec<PatternField> = fields_with_child_info
.iter()
.map(|(f, _)| f.clone())
.collect();
// Skip if this matches a pattern (already generated)
if pattern_lookup.contains_key(&fields)
&& pattern_lookup.get(&fields) != Some(&name.to_string())
{
return;
}
if generated.contains(name) {
return;
}
generated.insert(name.to_string());
writeln!(output, "class {}:", name).unwrap();
writeln!(output, " \"\"\"Catalog tree node.\"\"\"").unwrap();
writeln!(output, " ").unwrap();
writeln!(
output,
" def __init__(self, client: BrkClientBase, base_path: str = ''):"
)
.unwrap();
for ((field, child_fields_opt), (child_name, child_node)) in
fields_with_child_info.iter().zip(children.iter())
{
// For generic patterns, extract the value type from child fields
let generic_value_type = child_fields_opt
.as_ref()
.and_then(|cf| metadata.get_generic_value_type(&field.rust_type, cf));
let py_type = field_to_python_type_with_generic_value(
field,
metadata,
false,
generic_value_type.as_deref(),
);
let field_name_py = to_snake_case(&field.name);
if metadata.is_pattern_type(&field.rust_type) {
// Check if the pattern is parameterizable
let pattern = metadata
.structural_patterns
.iter()
.find(|p| p.name == field.rust_type);
let is_parameterizable = pattern.map(|p| p.is_parameterizable()).unwrap_or(false);
if is_parameterizable {
// Get the metric base from the first leaf descendant
let metric_base = get_pattern_instance_base(child_node, child_name);
writeln!(
output,
" self.{}: {} = {}(client, '{}')",
field_name_py, py_type, field.rust_type, metric_base
)
.unwrap();
} else {
writeln!(
output,
" self.{}: {} = {}(client, f'{{base_path}}/{}')",
field_name_py, py_type, field.rust_type, field.name
)
.unwrap();
}
} else if field_uses_accessor(field, metadata) {
// Leaf with accessor - get actual metric path from leaf
let metric_path = if let TreeNode::Leaf(leaf) = child_node {
format!("/{}", leaf.name())
} else {
format!("{{base_path}}/{}", field.name)
};
let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap();
if metric_path.contains("{base_path}") {
writeln!(
output,
" self.{}: {} = {}(client, f'{}')",
field_name_py, py_type, accessor.name, metric_path
)
.unwrap();
} else {
writeln!(
output,
" self.{}: {} = {}(client, '{}')",
field_name_py, py_type, accessor.name, metric_path
)
.unwrap();
}
} else {
// Leaf without accessor - get actual metric path from leaf
let metric_path = if let TreeNode::Leaf(leaf) = child_node {
format!("/{}", leaf.name())
} else {
format!("{{base_path}}/{}", field.name)
};
if metric_path.contains("{base_path}") {
writeln!(
output,
" self.{}: {} = MetricNode(client, f'{}')",
field_name_py, py_type, metric_path
)
.unwrap();
} else {
writeln!(
output,
" self.{}: {} = MetricNode(client, '{}')",
field_name_py, py_type, metric_path
)
.unwrap();
}
}
}
writeln!(output).unwrap();
// Generate child classes
for (child_name, child_node) in children {
if let TreeNode::Branch(grandchildren) = child_node {
let child_fields = get_node_fields(grandchildren, pattern_lookup);
if !pattern_lookup.contains_key(&child_fields) {
let child_class_name = format!("{}_{}", name, to_pascal_case(child_name));
generate_tree_class(
output,
&child_class_name,
child_node,
pattern_lookup,
metadata,
generated,
);
}
}
}
}
}
/// Generate the main client class
fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) {
writeln!(output, "class BrkClient(BrkClientBase):").unwrap();
writeln!(
output,
" \"\"\"Main BRK client with catalog tree and API methods.\"\"\""
)
.unwrap();
writeln!(output, " ").unwrap();
writeln!(
output,
" def __init__(self, base_url: str = 'http://localhost:3000', timeout: float = 30.0):"
)
.unwrap();
writeln!(output, " super().__init__(base_url, timeout)").unwrap();
writeln!(output, " self.tree = CatalogTree(self)").unwrap();
writeln!(output).unwrap();
// Generate API methods
generate_api_methods(output, endpoints);
}
/// Generate API methods from OpenAPI endpoints
fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
for endpoint in endpoints {
if !endpoint.should_generate() {
continue;
}
let method_name = endpoint_to_method_name(endpoint);
let return_type = endpoint
.response_type
.as_deref()
.map(js_type_to_python)
.unwrap_or_else(|| "Any".to_string());
// Build method signature
let params = build_method_params(endpoint);
writeln!(
output,
" def {}(self{}) -> {}:",
method_name, params, return_type
)
.unwrap();
// Docstring
if let Some(summary) = &endpoint.summary {
writeln!(output, " \"\"\"{}\"\"\"", summary).unwrap();
}
// Build path
let path = build_path_template(&endpoint.path, &endpoint.path_params);
if endpoint.query_params.is_empty() {
writeln!(output, " return self.get(f'{}')", path).unwrap();
} else {
writeln!(output, " params = []").unwrap();
for param in &endpoint.query_params {
// Use safe name for Python variable, original name for API query parameter
let safe_name = escape_python_keyword(&param.name);
if param.required {
writeln!(
output,
" params.append(f'{}={{{}}}')",
param.name, safe_name
)
.unwrap();
} else {
writeln!(
output,
" if {} is not None: params.append(f'{}={{{}}}')",
safe_name, param.name, safe_name
)
.unwrap();
}
}
writeln!(output, " query = '&'.join(params)").unwrap();
writeln!(
output,
" return self.get(f'{}{{\"?\" + query if query else \"\"}}')",
path
)
.unwrap();
}
writeln!(output).unwrap();
}
}
fn endpoint_to_method_name(endpoint: &Endpoint) -> String {
if let Some(op_id) = &endpoint.operation_id {
return to_snake_case(op_id);
}
// Include path parameters as "by_{param}" to differentiate endpoints
let mut parts = Vec::new();
for segment in endpoint.path.split('/').filter(|s| !s.is_empty()) {
if let Some(param) = segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
parts.push(format!("by_{}", param));
} else {
parts.push(segment.to_string());
}
}
to_snake_case(&format!("get_{}", parts.join("_")))
}
/// Convert JS-style type to Python type (e.g., "Txid[]" -> "List[Txid]")
fn js_type_to_python(js_type: &str) -> String {
if let Some(inner) = js_type.strip_suffix("[]") {
format!("List[{}]", js_type_to_python(inner))
} else {
js_type.to_string()
}
}
fn build_method_params(endpoint: &Endpoint) -> String {
let mut params = Vec::new();
for param in &endpoint.path_params {
let safe_name = escape_python_keyword(&param.name);
params.push(format!(", {}: str", safe_name));
}
for param in &endpoint.query_params {
let safe_name = escape_python_keyword(&param.name);
if param.required {
params.push(format!(", {}: str", safe_name));
} else {
params.push(format!(", {}: Optional[str] = None", safe_name));
}
}
params.join("")
}
fn build_path_template(path: &str, path_params: &[super::Parameter]) -> String {
let mut result = path.to_string();
for param in path_params {
let placeholder = format!("{{{}}}", param.name);
let interpolation = format!("{{{{{}}}}}", param.name);
result = result.replace(&placeholder, &interpolation);
}
result
}