mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-24 14:49:58 -07:00
210 lines
8.1 KiB
Rust
210 lines
8.1 KiB
Rust
//! Python API method generation.
|
|
|
|
use std::fmt::Write;
|
|
|
|
use crate::{Endpoint, Parameter, escape_python_keyword, generators::{normalize_return_type, write_description}, to_snake_case};
|
|
|
|
use super::client::generate_class_constants;
|
|
use super::types::js_type_to_python;
|
|
|
|
/// Generate the main client class
|
|
pub fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) {
|
|
writeln!(output, "class BrkClient(BrkClientBase):").unwrap();
|
|
writeln!(
|
|
output,
|
|
" \"\"\"Main BRK client with metrics tree and API methods.\"\"\""
|
|
)
|
|
.unwrap();
|
|
writeln!(output).unwrap();
|
|
|
|
// Generate class-level constants
|
|
generate_class_constants(output);
|
|
|
|
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.metrics = MetricsTree(self)").unwrap();
|
|
writeln!(output).unwrap();
|
|
|
|
// Generate metric() method for dynamic metric access
|
|
writeln!(output, " def metric(self, metric: str, index: Index) -> MetricEndpointBuilder[Any]:").unwrap();
|
|
writeln!(output, " \"\"\"Create a dynamic metric endpoint builder for any metric/index combination.").unwrap();
|
|
writeln!(output).unwrap();
|
|
writeln!(output, " Use this for programmatic access when the metric name is determined at runtime.").unwrap();
|
|
writeln!(output, " For type-safe access, use the `metrics` tree instead.").unwrap();
|
|
writeln!(output, " \"\"\"").unwrap();
|
|
writeln!(output, " return MetricEndpointBuilder(self, metric, index)").unwrap();
|
|
writeln!(output).unwrap();
|
|
|
|
// Generate helper methods
|
|
writeln!(output, " def index_to_date(self, index: Index, i: int) -> date:").unwrap();
|
|
writeln!(output, " \"\"\"Convert an index value to a date for date-based indexes.\"\"\"").unwrap();
|
|
writeln!(output, " return index_to_date(index, i)").unwrap();
|
|
writeln!(output).unwrap();
|
|
writeln!(output, " def is_date_index(self, index: Index) -> bool:").unwrap();
|
|
writeln!(output, " \"\"\"Check if an index type is date-based.\"\"\"").unwrap();
|
|
writeln!(output, " return is_date_index(index)").unwrap();
|
|
writeln!(output).unwrap();
|
|
|
|
// Generate API methods
|
|
generate_api_methods(output, endpoints);
|
|
}
|
|
|
|
/// Generate API methods from OpenAPI endpoints
|
|
pub 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 base_return_type = normalize_return_type(
|
|
&endpoint
|
|
.response_type
|
|
.as_deref()
|
|
.map(js_type_to_python)
|
|
.unwrap_or_else(|| "Any".to_string()),
|
|
);
|
|
|
|
let return_type = if endpoint.supports_csv {
|
|
format!("Union[{}, str]", base_return_type)
|
|
} else {
|
|
base_return_type
|
|
};
|
|
|
|
// Build method signature
|
|
let params = build_method_params(endpoint);
|
|
writeln!(
|
|
output,
|
|
" def {}(self{}) -> {}:",
|
|
method_name, params, return_type
|
|
)
|
|
.unwrap();
|
|
|
|
// Docstring
|
|
match (&endpoint.summary, &endpoint.description) {
|
|
(Some(summary), Some(desc)) if summary != desc => {
|
|
writeln!(output, " \"\"\"{}.", summary.trim_end_matches('.')).unwrap();
|
|
writeln!(output).unwrap();
|
|
write_description(output, desc, " ", "");
|
|
}
|
|
(Some(summary), _) => {
|
|
writeln!(output, " \"\"\"{}", summary).unwrap();
|
|
}
|
|
(None, Some(desc)) => {
|
|
// First line includes opening quotes
|
|
let mut lines = desc.lines();
|
|
if let Some(first) = lines.next() {
|
|
writeln!(output, " \"\"\"{}", first).unwrap();
|
|
}
|
|
for line in lines {
|
|
if line.is_empty() {
|
|
writeln!(output).unwrap();
|
|
} else {
|
|
writeln!(output, " {}", line).unwrap();
|
|
}
|
|
}
|
|
}
|
|
(None, None) => {
|
|
write!(output, " \"\"\"").unwrap();
|
|
}
|
|
}
|
|
writeln!(output).unwrap();
|
|
writeln!(output, " Endpoint: `{} {}`\"\"\"", endpoint.method.to_uppercase(), endpoint.path).unwrap();
|
|
|
|
// Build path
|
|
let path = build_path_template(&endpoint.path, &endpoint.path_params);
|
|
|
|
if endpoint.query_params.is_empty() {
|
|
if endpoint.path_params.is_empty() {
|
|
writeln!(output, " return self.get_json('{}')", path).unwrap();
|
|
} else {
|
|
writeln!(output, " return self.get_json(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(¶m.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,
|
|
" path = f'{}{{\"?\" + query if query else \"\"}}'",
|
|
path
|
|
)
|
|
.unwrap();
|
|
|
|
if endpoint.supports_csv {
|
|
writeln!(output, " if format == 'csv':").unwrap();
|
|
writeln!(output, " return self.get_text(path)").unwrap();
|
|
writeln!(output, " return self.get_json(path)").unwrap();
|
|
} else {
|
|
writeln!(output, " return self.get_json(path)").unwrap();
|
|
}
|
|
}
|
|
|
|
writeln!(output).unwrap();
|
|
}
|
|
}
|
|
|
|
fn endpoint_to_method_name(endpoint: &Endpoint) -> String {
|
|
to_snake_case(&endpoint.operation_name())
|
|
}
|
|
|
|
fn build_method_params(endpoint: &Endpoint) -> String {
|
|
let mut params = Vec::new();
|
|
// Path params are always required
|
|
for param in &endpoint.path_params {
|
|
let safe_name = escape_python_keyword(¶m.name);
|
|
let py_type = js_type_to_python(¶m.param_type);
|
|
params.push(format!(", {}: {}", safe_name, py_type));
|
|
}
|
|
// Required query params must come before optional ones (Python syntax requirement)
|
|
for param in &endpoint.query_params {
|
|
if param.required {
|
|
let safe_name = escape_python_keyword(¶m.name);
|
|
let py_type = js_type_to_python(¶m.param_type);
|
|
params.push(format!(", {}: {}", safe_name, py_type));
|
|
}
|
|
}
|
|
for param in &endpoint.query_params {
|
|
if !param.required {
|
|
let safe_name = escape_python_keyword(¶m.name);
|
|
let py_type = js_type_to_python(¶m.param_type);
|
|
params.push(format!(", {}: Optional[{}] = None", safe_name, py_type));
|
|
}
|
|
}
|
|
params.join("")
|
|
}
|
|
|
|
fn build_path_template(path: &str, path_params: &[Parameter]) -> String {
|
|
let mut result = path.to_string();
|
|
for param in path_params {
|
|
let placeholder = format!("{{{}}}", param.name);
|
|
// Use escaped name for Python variable interpolation in f-string
|
|
let safe_name = escape_python_keyword(¶m.name);
|
|
let interpolation = format!("{{{}}}", safe_name);
|
|
result = result.replace(&placeholder, &interpolation);
|
|
}
|
|
result
|
|
}
|